# Line Tabs

Tabs drawn as words with a line under the selected one. No pill and no plate: the strip sits on the rule of the header around it, and the line reads as that rule thickening under one word.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





<ComponentPreview name="line-tabs">
  <LineTabsDemo />
</ComponentPreview>

## Installation [#installation]

```bash
npx shadcn@latest add https://motif.sweetretry.com/r/line-tabs.json
```

## Usage [#usage]

```tsx
import {
  LineTabs,
  LineTabsContent,
  LineTabsList,
  LineTabsTrigger,
} from "@/components/ui/line-tabs";

export function Panel() {
  return (
    <LineTabs defaultValue="results">
      <div className="flex items-center justify-between border-b pl-6 pr-3">
        <LineTabsList aria-label="Sections">
          <LineTabsTrigger value="results">Results</LineTabsTrigger>
          <LineTabsTrigger value="files">Files</LineTabsTrigger>
        </LineTabsList>
        <CloseButton />
      </div>
      <LineTabsContent value="results">…</LineTabsContent>
      <LineTabsContent value="files">…</LineTabsContent>
    </LineTabs>
  );
}
```

The parts are Radix tabs under new names, so the selection works the way Radix's does:
controlled with `value` and `onValueChange`, or uncontrolled with `defaultValue`.

## Words, not a control [#words-not-a-control]

The selected tab is the word at full strength with a 2px line under it. The others are muted
words. That is the whole vocabulary: there is no plate behind the selection and no tray the
tabs sit in.

The choice is about what the strip is part of. A segmented control is a thing you operate,
so it earns a surface. Tabs in a panel header are part of the header: they name the
sections of the panel, and a pill inside a header is a second surface inside the first, one
more edge for the eye to read before it reaches the content. Words on the header's own rule
add none.

## The rule is the header's [#the-rule-is-the-headers]

`LineTabsList` draws no border. The rule under the strip belongs to the header the strip
sits in, and the selected line lands on it. A strip that drew its own rule would stack two
the moment it was dropped into a header that already had one, and would have no way to be
used in a place that has none.

So the pairing is the caller's: put a `border-b` on the row that holds the list, and put
the panel's own controls opposite the tabs in the same row. Every trigger is `h-12` and the
list stretches to it; pass a `className` to change the height for a tighter header.

## The line follows the word [#the-line-follows-the-word]

The line is an inset shadow in `currentColor`, and the selected tab's text is the
foreground, so the line is always exactly the colour of the word above it, in any theme,
without a token of its own. A count or a glyph inside the trigger is part of the word and
shares the line.

## Keyboard [#keyboard]

Focus is one tab stop with a roving tab: arrow keys move between tabs, Home and End jump to
the ends, and selection follows focus. A tab names its panel through `aria-controls`, and
the panel names the tab back, which Radix wires from the `value` you give both. Only the
selected panel is mounted.

## Why not Tabs [#why-not-tabs]

The inherited `Tabs` is a pill in a tray, and that look lives inside the component:
restyling it to a line means overriding the tray, the active fill, the border and the dark
variants of each, and any one of those drifts the next time the base changes. This is the
same primitive with a different skin, kept as its own component rather than a long
`className` that every caller would have to repeat.

| Part              | Notes                                                  |
| ----------------- | ------------------------------------------------------ |
| `LineTabs`        | The root. `value` / `onValueChange`, or `defaultValue` |
| `LineTabsList`    | The strip. As wide as its tabs; draws no rule          |
| `LineTabsTrigger` | One tab. `h-12`, the line under it when selected       |
| `LineTabsContent` | A panel, mounted only while its tab is selected        |

## Component source [#component-source]

<ComponentSource name="line-tabs" src="registry/new-york/line-tabs.tsx" title="line-tabs.tsx" />
