Installation
Registry — recommended
Copies the component into your configured shadcn UI directory and installs Motion.
npx shadcn@latest add https://suluu.site/r/fluid-tabs.jsonnpm package
For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.
pnpm add suluu motionUsage
Supply your own icon for every tab and give the tablist a stable accessible name. Use item-level accent colors for distinct destinations, or omit them to share the theme accent.
import { FluidTabs } from "@/components/ui/fluid-tabs"
const tabs = [
{
value: "inbox",
label: "Inbox",
accentColor: "#087cf0",
id: "inbox-tab",
panelId: "inbox-panel",
icon: (
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
<path d="M3 6.5h18v12H3zM4 8l8 6 8-6" stroke="currentColor" strokeWidth="2" />
</svg>
),
},
{
value: "planner",
label: "Planner",
accentColor: "#f0b429",
id: "planner-tab",
panelId: "planner-panel",
icon: (
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
<path d="M4 5h16v15H4zM8 3v4m8-4v4M4 10h16" stroke="currentColor" strokeWidth="2" />
</svg>
),
},
{
value: "alerts",
label: "Alerts",
accentColor: "#f0443e",
id: "alerts-tab",
panelId: "alerts-panel",
icon: (
<svg aria-hidden="true" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2a6 6 0 0 0-6 6v4l-2 4v2h16v-2l-2-4V8a6 6 0 0 0-6-6Zm-2 18h4a2 2 0 0 1-4 0Z" />
</svg>
),
},
]
export function WorkspaceTabs() {
return (
<FluidTabs
aria-label="Workspace"
defaultValue="inbox"
tabs={tabs}
/>
)
}// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";
// component.tsx
import { FluidTabs } from "suluu/fluid-tabs"
<FluidTabs
aria-label="Workspace"
defaultValue="inbox"
tabs={tabs}
/>Controlled tabs and panels
FluidTabs owns the triggers only. When it switches product content, connect each trigger and panel with matching ids and keep inactive panels hidden.
const [tab, setTab] = useState("inbox")
<>
<FluidTabs
aria-label="Workspace"
onValueChange={setTab}
tabs={tabs}
value={tab}
/>
{tabs.map((item) => (
<section
aria-labelledby={item.id}
hidden={tab !== item.value}
id={item.panelId}
key={item.value}
role="tabpanel"
>
{item.label} content
</section>
))}
</>Sizes
The tablist is built from a single em geometry, so a size is only a change of type scale. Override--suluu-fluid-tabs-font-sizeon the tablist to scale it continuously instead. The active pill also clamps itself to the viewport, so a long label narrows rather than pushing the row off a small screen.
// Three type scales; every dimension is derived from them.
<FluidTabs size="sm" tabs={tabs} />
<FluidTabs tabs={tabs} /> // md, the default
<FluidTabs size="lg" tabs={tabs} />
// Or scale it continuously by setting the em basis yourself.
<FluidTabs
style={{ "--suluu-fluid-tabs-font-size": "1.125rem" }}
tabs={tabs}
/>In context
A communication workspace uses the expanding active tab to preserve context without giving every destination a permanent label.
Workspace
Everything waiting for you today.
3 unread
The launch review is ready
Mara left two notes on the final interaction pass.
Next up
Motion polish · 10:30
A quiet half hour reserved for the final spring tuning.
Just now
Preview deployment finished
The latest workspace build is ready for review.
Props
FluidTabs accepts safe native div attributes and forwards its ref to the tablist. Each item may also set thedisabled state without disabling its siblings.
| Prop | Type | Default | Description |
|---|---|---|---|
| tabs | FluidTab[] | — | Tabs rendered in visual and keyboard order. |
| tabs[].value | string | — | Unique value used for selection. |
| tabs[].label | string | — | Visible and accessible tab name. |
| tabs[].icon | ReactNode | — | Custom icon-sized content. |
| tabs[].accentColor | string | Theme accent | CSS color used while that tab is active. |
| tabs[].id / panelId | string | Generated / — | Optional ids connecting a trigger to its tabpanel. |
| value | string | — | Controlled active value. |
| defaultValue | string | First enabled tab | Initial uncontrolled active value. |
| onValueChange | (value) => void | — | Runs when an interaction requests another tab. |
| size | "sm" | "md" | "lg" | "md" | Type scale the whole control is sized from. |
| disabled | boolean | false | Disables the complete tablist. |
| className | string | — | Class name applied to the tablist. |
Theming
Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.
--suluu-fluid-tabs-font-sizeEm basis every dimension is derived from.--suluu-fluid-tabs-backgroundIndividual circle and pill surface.--suluu-fluid-tabs-foregroundInactive icon color.--suluu-fluid-tabs-borderSurface edge.--suluu-fluid-tabs-accentFallback active icon and label color.--suluu-fluid-tabs-ringKeyboard focus ring.--suluu-fluid-tabs-offsetColor behind the focus ring offset.--suluu-fluid-tabs-shadowCircle and pill elevation.--suluu-fluid-tabs-shimmer-intensityStrength of the sheen that travels the revealed label as it opens.The root exposes data-state for the active value. Triggers exposedata-state asactive orinactive. An item’saccentColor overrides the fallback accent for that trigger only.
Accessibility
FluidTabs is a horizontal tablist of native buttons. Left and Right arrows move and activate selection, Home and End jump to the first and last enabled tabs, and only the active tab sits in the page tab order.
The label remains the accessible name even while visually collapsed. With reduced motion, expansion, label movement, and color transitions become immediate while active state remains clear.
The sweep of light is decorative: it is hidden from assistive technology, never intercepts pointer events, and is not rendered at all under reduced motion. Selection stays legible through the accent color alone.