@fvc/tabs
v1.1.0
Published
`@fvc/tabs` provides FE-VIS styled tabs on top of Ant Design. It preserves Ant Design's tablist, tabpanel, keyboard, focus, and panel-mounting behavior while applying the AVIS UI kit pill design.
Downloads
1,860
Readme
@fvc/tabs
@fvc/tabs provides FE-VIS styled tabs on top of Ant Design. It preserves
Ant Design's tablist, tabpanel, keyboard, focus, and panel-mounting behavior
while applying the AVIS UI kit pill design.
Installation
bun add @fvc/tabsPeer Dependencies
bun add react antdThe draggable prop additionally requires @dnd-kit:
bun add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilitiesImport
import { Tabs } from '@fvc/tabs';Quick Start
import { useState } from 'react';
import { Tabs } from '@fvc/tabs';
const items = [
{ key: 'overview', label: 'Overview', children: <Overview /> },
{ key: 'details', label: 'Details', children: <Details /> },
{ key: 'history', label: 'History', children: <History /> },
];
export function PageHeader() {
const [activeKey, setActiveKey] = useState('overview');
return <Tabs items={items} activeKey={activeKey} onChange={setActiveKey} />;
}Usage
Controlled
activeKey remains the source of truth. Clicking another tab calls
onChange, but the visible tab changes only when the parent supplies a new
activeKey.
<Tabs items={items} activeKey={activeKey} onChange={setActiveKey} />Uncontrolled
<Tabs items={items} defaultActiveKey="details" />Figma Size M (default)
Size M uses Roboto Bold at 14px/16px with 8px vertical and 12px
horizontal padding. The browser computes the final item height from those
values.
<Tabs items={items} size="middle" />Figma Size L
Size L uses Roboto Bold at 20px/20px with 10px vertical and 12px
horizontal padding. The browser computes the final item height from those
values.
<Tabs items={items} size="large" />Disabled Item
Disabled click and keyboard behavior remains owned by Ant Design.
<Tabs
items={[
{ key: 'active', label: 'Active' },
{ key: 'locked', label: 'Locked', disabled: true },
]}
/>Icon
icon accepts any ReactNode. The package does not require an icon library.
<Tabs
items={[
{ key: 'home', label: 'Home', icon: <HomeIcon /> },
{ key: 'history', label: 'History', icon: <HistoryIcon /> },
]}
/>Extra Content
<Tabs
items={items}
tabBarExtraContent={{
left: <span>Filters</span>,
right: <button type="button">Add</button>,
}}
/>Without Content Panels
Omit children when the active key controls content outside this component.
<Tabs
items={[
{ key: 'day', label: 'Day' },
{ key: 'week', label: 'Week' },
{ key: 'month', label: 'Month' },
]}
activeKey={range}
onChange={setRange}
/>Draggable
const [items, setItems] = useState(initialItems);
<Tabs items={items} draggable onOrderChange={setItems} />;Enables drag-and-drop reordering via @dnd-kit —
the same approach as the
official Ant Design drag-and-drop tabs example.
onOrderChange is called with the reordered TabItem[] after each drop; the
consumer owns committing that order back into items.
Props
| Prop | Type | Default | Description |
| ------------------------ | ------------------------------------------------------ | ---------- | ------------------------------------------------ |
| items | AntTabsProps['items'] | - | Required tab descriptors. |
| activeKey | string | - | Controlled active key. |
| defaultActiveKey | string | first item | Initial uncontrolled active key. |
| onChange | (key: string) => void | - | Called with the selected key. |
| size | AntTabsProps['size'] | middle | Uses Ant Design size names. |
| tabBarExtraContent | ReactNode \| { left?: ReactNode; right?: ReactNode } | - | Content around the tab list. |
| destroyInactiveTabPane | boolean | false | Removes inactive panels when enabled. |
| draggable | boolean | false | Enables drag-and-drop reordering via @dnd-kit. |
| onOrderChange | (items: TabItem[]) => void | - | Called with the reordered items after each drop. |
| className | string | - | Additional root class. |
| style | TabsStyle | - | Root styles with typed --tabs-* variables. |
| testId | string | - | Root data-testid. |
TabItem
| Field | Type | Description |
| ---------- | ----------- | ------------------------------- |
| key | string | Required unique key. |
| label | ReactNode | Required label. |
| children | ReactNode | Optional panel content. |
| disabled | boolean | Prevents selection. |
| icon | ReactNode | Optional icon before the label. |
Panel Mounting
Inactive panels remain mounted by default (destroyInactiveTabPane=false).
This preserves form values and component state but also retains subscriptions
and expensive resources. Set destroyInactiveTabPane when cleanup is more
important than state preservation. Lazy code splitting remains the consumer's
responsibility.
Theming
All design values are declared in src/styles/variables.scss, matching the
style-file layout used by @fvc/input.
Inline overrides are type-safe:
<Tabs
items={items}
style={{
'--tabs-item-active-bg-color': '#24568a',
'--tabs-item-hover-bg-color': '#f2f2f2',
}}
/>Container
| Variable | Default |
| ---------------------- | ------------------ |
| --tabs-bg-color | var(--neutral-0) |
| --tabs-border-width | 1px |
| --tabs-border-color | var(--blue-250) |
| --tabs-border-radius | 18px |
Item
| Variable | Default |
| --------------------------- | ------------------- |
| --tabs-item-padding-x | 9px |
| --tabs-item-border-width | 3px |
| --tabs-item-border-color | transparent |
| --tabs-item-border-radius | 32px |
| --tabs-item-text-color | var(--black-1000) |
| --tabs-item-fw | 700 |
Selected Item
| Variable | Default |
| --------------------------------- | --------------------------- |
| --tabs-item-active-bg-color | var(--blue-400) |
| --tabs-item-active-text-color | var(--white-text-color-0) |
| --tabs-item-active-border-color | var(--gray-100) |
Hover Item
Hover is the only state sourced from the adjacent Figma <Toggles />
component because the Figma <Tabs /> variants do not expose a hover state.
| Variable | Default |
| -------------------------------- | ----------------------------- |
| --tabs-item-hover-bg-color | var(--gray-50) (#f2f2f2) |
| --tabs-item-hover-border-color | var(--gray-100) (#fafafa) |
Size M
| Variable | Default |
| ------------------------------ | -------- |
| --tabs-font-family | var(--fvc-font-body, sans-serif) |
| --tabs-middle-size-fz | 14px |
| --tabs-middle-size-lh | 16px |
| --tabs-middle-size-padding-y | 4px |
The CSS padding values compensate for the real 3px item border and 1px
container border. This preserves Figma's visual 8px padding and 32px
overall height.
Size L
| Variable | Default |
| ----------------------------- | ------- |
| --tabs-large-size-fz | 20px |
| --tabs-large-size-lh | 20px |
| --tabs-large-size-padding-y | 6px |
The 7px CSS padding preserves Figma's visual 10px padding and 40px
overall height after the real borders are included.
Figma Mapping
- Tabs variants: M/L and counts 2-6
- Hover only: adjacent
<Toggles />node27384:17118/27384:17120
The transparent base item border reserves the same space used by the selected
and hover strokes. With the Figma Secondary label, two M items remain
182x32 and two L items remain 272x40.
Accessibility
The wrapper keeps Ant Design's tablist, tab, and tabpanel roles,
aria-selected, aria-controls, aria-labelledby, disabled handling,
keyboard navigation, and focus-visible behavior. Horizontal tabs use
Left/Right; Home/End jump to the edges; Enter/Space activate the
keyboard-focused tab.
Development
bun run lint
bun run type-check
bun run test
bun run build