Components / Navigation

Tabs

Line, pill or document tabs with arrow-key activation and lazy panels. The document variant adds closable, dirty and link tabs, drag and keyboard reordering (Ctrl+Shift+Arrow), overflow scrolling with an all-tabs menu, and a context menu.

@ruckc/truss-ui Navigation

Import

import { Tabs } from '@ruckc/truss-ui';

Playground

variant
count
value

Recipe: a tab strip without a panel

Leave out panel (or pass panel={false}) and only the strip renders, for an app that keeps its own content, such as an editor whose buffers must stay mounted. Keyboard, close, reorder and overflow behave exactly as before. Without a panel the tabs have no aria-controls unless you provide controls: return the id of the element you render for each tab, and give that element role="tabpanel" and a name (the tab's label). Render only the active content, keyed by value, so the ids you return exist when they are referenced.

export const answer = 42;
<Tabs variant="document" bind:tabs={files} bind:value label="Open files"
      controls={(id) => 'view-' + id} onclose={(id) => (files = files.filter((f) => f.id !== id))} />

{#key value}
  <div role="tabpanel" id={'view-' + value} aria-label={labelOf(value)}>
    <Editor file={value} />   <!-- your own content, keyed by the active tab -->
  </div>
{/key}

Recipe: a panel that fills its parent

With fill the component is a flex column as tall as its parent, and the panel is a flex child with no padding that takes the remaining height (and scrolls if its content is taller). Give the parent a height. It works for every variant.

<div style="height: 320px">
  <Tabs fill variant="document" bind:tabs={files} bind:value>
    {#snippet panel(id)}<Editor file={id} />{/snippet}
  </Tabs>
</div>

API

Document variant: renders the tab as a real link (client routing, open in new tab).

PropTypeDefaultDescription
tabs bindable ({ id: string; label: string; icon?: IconSource; disabled?: boolean; closable?: boolean; dirty?: boolean; href?: string; })[]—In the document variant the list is reordered in place (and `onreorder` fires), so bind it or apply `onreorder` yourself. Document variant: shows a close button (and enables Delete / Ctrl+W, middle-click and the context-menu entries). Document variant: unsaved changes (a dot, plus "unsaved changes" in the accessible name). Document variant: renders the tab as a real link (client routing, open in new tab).
value bindable string—The current value. Bind it with `bind:value`.
panel snippet Snippet<[id: string]> | false—Rendered for the active tab only (lazy). Omit it (or pass `false`) for a headless strip: only the tabs render, and your app shows the content for `value` wherever it likes. Use `controls` so each tab's `aria-controls` points at that content.
fill booleanfalseThe panel is a flex child with no padding that fills the height of the parent (give the parent a height). Has no effect without a panel.
controls (id: string) => string | undefined—Headless only: the id of the element you render for a tab, used for its `aria-controls`. Give that element `role="tabpanel"` and `aria-labelledby` the tab's id.
label string'Sections'Visible label shown above the control.
variant 'line' | 'pill' | 'document''line'Visual style of the control.
onclose event(id: string) => void—Document variant: a closable tab was asked to close (button, middle-click, Delete / Ctrl+W, context menu). Remove it from `tabs` in response.
onreorder event(ids: string[]) => void—Document variant: the new order of tab ids after a drag or Ctrl+Shift+Arrow.
tabExtra snippet Snippet<[tab: ({ id: string; label: string; icon?: IconSource; disabled?: boolean; closable?: boolean; dirty?: boolean; href?: string; })]>—Document variant only: decorative content (presence avatars, a status dot, a count badge) rendered between the label and the close button. It is `aria-hidden` and `inert`, so it is never focusable, never part of the tab's accessible name, and never intercepts clicks or drags; put interactive content elsewhere. Expose its meaning with `tabExtraLabel`. Document variant: shows a close button (and enables Delete / Ctrl+W, middle-click and the context-menu entries). Document variant: unsaved changes (a dot, plus "unsaved changes" in the accessible name). Document variant: renders the tab as a real link (client routing, open in new tab).
tabExtraLabel (tab: ({ id: string; label: string; icon?: IconSource; disabled?: boolean; closable?: boolean; dirty?: boolean; href?: string; })) => string | undefined—Document variant: text appended to the tab's accessible name (visually hidden), e.g. `3 collaborators`. Empty or undefined adds nothing. Document variant: shows a close button (and enables Delete / Ctrl+W, middle-click and the context-menu entries). Document variant: unsaved changes (a dot, plus "unsaved changes" in the accessible name). Document variant: renders the tab as a real link (client routing, open in new tab).
/docs/tabs Truss UI · MIT