Skip to contentSkip to navigationSkip to topbar
Figma
Star

Tabs Primitive

Version 2.0.1GithubStorybook

An unstyled and accessible basis upon which to style a tabset.


Component preview theme
const HorizontalExample = () => {
const tab = useTabPrimitiveState();
return (
<>
<TabPrimitiveList {...tab} aria-label="My tabs">
<TabPrimitive {...tab}>
Tab 1
</TabPrimitive>
<TabPrimitive {...tab} disabled>
Tab 2 (disabled)
</TabPrimitive>
<TabPrimitive {...tab}>
Tab 3
</TabPrimitive>
</TabPrimitiveList>
<TabPrimitivePanel {...tab}>Tab 1</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 2</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 3</TabPrimitivePanel>
</>
)
};
render(
<HorizontalExample />
)

Guidelines

Guidelines page anchor

About the Tabs Primitive

About the Tabs Primitive page anchor

The Tabs primitive is an unstyled, functional version of a tabset component. It can be used to build a component following the WAI-ARIA Tabs Pattern(link takes you to an external page). Our Tabs component is built on top of this primitive.

This unstyled primitive is to be used when the styled Tabs provided by Paste doesnt meet the requirements needed to solve a unique customer problem. At that point, you are welcome to fall back to this functional primitive to roll your own styled Tabs while still providing a functional and accessible experience to your customers.

This primitive should be used to compose all custom Tabs to ensure accessibility and upgrade paths.

(warning)

Before you roll your own tabset...

We strongly suggest that all components built on top of this primitive get reviewed by the Design Systems team and go through the UX Review process to ensure an excellent experience for our customers.

Horizontal Tabset

Horizontal Tabset page anchor
Component preview theme
const HorizontalExample = () => {
const tab = useTabPrimitiveState();
return (
<>
<TabPrimitiveList {...tab} aria-label="My tabs">
<TabPrimitive {...tab}>
Tab 1
</TabPrimitive>
<TabPrimitive {...tab} disabled>
Tab 2 (disabled)
</TabPrimitive>
<TabPrimitive {...tab}>
Tab 3
</TabPrimitive>
</TabPrimitiveList>
<TabPrimitivePanel {...tab}>Tab 1</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 2</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 3</TabPrimitivePanel>
</>
)
};
render(
<HorizontalExample />
)
Component preview theme
const VerticalExample = () => {
const tab = useTabPrimitiveState({orientation: 'vertical'});
return (
<Stack orientation="horizontal" spacing="space40">
<Box>
<TabPrimitiveList {...tab} aria-label="My tabs">
<Stack orientation="vertical" spacing="space20">
<TabPrimitive {...tab}>
Tab 1
</TabPrimitive>
<TabPrimitive {...tab}>
Tab 2
</TabPrimitive>
<TabPrimitive {...tab}>
Tab 3
</TabPrimitive>
</Stack>
</TabPrimitiveList>
</Box>
<Box>
<TabPrimitivePanel {...tab}>Tab 1</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 2</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 3</TabPrimitivePanel>
</Box>
</Stack>
)
};
render(
<VerticalExample />
)

The tab primitive can be styled using Paste components and tokens. By using the as prop, we can change the underlying element and combine it with another component. In the example below we're combining the TabPrimitive with a custom Box component. We are rendering the TabPrimitive as CustomTab, passing down the necessary props from the state hook and combining those with Box styling props.

Component preview theme
const CustomTab = React.forwardRef((props, ref) => (
<Box
as="div"
ref={ref}
borderBottomColor="transparent"
borderRadius="borderRadius0"
borderBottomStyle="solid"
borderBottomWidth="borderWidth20"
cursor="pointer"
padding="space20"
outline="none"
_selected={{
borderColor:"colorBorderPrimary"
}}
_focus={{
textDecoration: 'underline'
}}
_disabled={{
color: 'colorTextWeaker'
}}
{...props}
/>
));
const StyledExample = () => {
const tab = useTabPrimitiveState();
return (
<>
<TabPrimitiveList {...tab} aria-label="My tabs">
<Stack orientation="horizontal" spacing="space20">
<TabPrimitive {...tab} as={CustomTab}>
Tab 1
</TabPrimitive>
<TabPrimitive {...tab} disabled as={CustomTab}>
Tab 2
</TabPrimitive>
<TabPrimitive {...tab} as={CustomTab}>
Tab 3
</TabPrimitive>
</Stack>
</TabPrimitiveList>
<TabPrimitivePanel {...tab}>Tab 1</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 2</TabPrimitivePanel>
<TabPrimitivePanel {...tab}>Tab 3</TabPrimitivePanel>
</>
)
};
render(
<StyledExample />
)

This package is a wrapper around the Reakit Tab(link takes you to an external page). If youre wondering why we wrapped that package into our own, we reasoned that it would be best for our consumers developer experience. For example:

  • By wrapping a library in @twilio-paste/x-primitive, any update or swap of the underlying library would only be a version bump in the package.json file for the primitive. Without this, if we want to migrate the underlying library in the future, Twilio products that depend on this primitive would need to replace all occurrences ofimport … from 'x-package'toimport … from '@some-new/package'.
  • We can more strictly enforce semver and backwards compatibility than some of our dependencies.
  • We can control when to provide an update and which versions we allow, to help reduce potential bugs our consumers may face.
  • We can control which APIs we expose. For example, we may chose to enable or disable usage of certain undocumented APIs.
yarn add @twilio-paste/tabs-primitive - or - yarn add @twilio-paste/core
import {
  useTabPrimitiveState,
  TabPrimitiveList,
  TabPrimitive,
  TabPrimitivePanel,
} from '@twilio-paste/core/tabs-primitive';

const HorizontalExample = () => {
  const tab = useTabPrimitiveState();
  return (
    <>
      <TabPrimitiveList {...tab} aria-label="My tabs">
        <TabPrimitive {...tab}>Tab 1</TabPrimitive>
        <TabPrimitive {...tab} disabled>
          Tab 2 (disabled)
        </TabPrimitive>
        <TabPrimitive {...tab}>Tab 3</TabPrimitive>
      </TabPrimitiveList>
      <TabPrimitivePanel {...tab}>Tab 1</TabPrimitivePanel>
      <TabPrimitivePanel {...tab}>Tab 2</TabPrimitivePanel>
      <TabPrimitivePanel {...tab}>Tab 3</TabPrimitivePanel>
    </>
  );
};

This props list is a scoped version of the properties available from the Reakit Tab(link takes you to an external page) package.

baseId string

ID that will serve as a base for all the items IDs.

rtl boolean

Determines how next and previous functions will behave. If rtl is set to true, they will be inverted. You still need to set dir="rtl" on HTML/CSS.

orientation "horizontal" | "vertical" | undefined

Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus:

It doesn't have any effect on two-dimensional composites.

currentId string | null | undefined

The current focused item id.

loop boolean | "horizontal" | "vertical"

Whether keyboard navigation loops back to the beginning.

wrap boolean | "horizontal" | "vertical"

If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.

selectedId string | null | undefined

The current selected tab's id.

manual boolean

Whether the tab selection should be manual.


disabled boolean | undefined

Same as the HTML attribute.

focusable boolean | undefined

When an element is disabled, it may still be focusable. It works similarly to readOnly on form elements. In this case, only aria-disabled will be set.

id string | undefined

Same as the HTML attribute.

(information)

State props

These props are returned by the state hook. You can spread them into this component (...state) or pass them separately. You can also provide these props from your own state logic.

baseId string

ID that will serve as a base for all the items IDs.

orientation "horizontal" | "vertical" | undefined

Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus:

It doesn't have any effect on two-dimensional composites.

currentId string | null | undefined

The current focused item id.

items Item[]

Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called.

setCurrentId (value: SetStateAction<string | null | undefined>) => void

Sets currentId.

registerItem (item: Item) => void

Registers a composite item.

unregisterItem (id: string) => void

Unregisters a composite item.

next (unstable_allTheWay?: boolean | undefined) => void

Moves focus to the next item.

previous (unstable_allTheWay?: boolean | undefined) => void

Moves focus to the previous item.

up (unstable_allTheWay?: boolean | undefined) => void

Moves focus to the item above.

down (unstable_allTheWay?: boolean | undefined) => void

Moves focus to the item below.

first () => void

Moves focus to the first item.

last () => void

Moves focus to the last item.

manual boolean

Whether the tab selection should be manual.

selectedId string | null | undefined

The current selected tab's id.

panels Item[]

Lists all the panels.

select (id: string | null) => void

Moves into and selects a tab by its id.


disabled boolean | undefined

Same as the HTML attribute.

focusable boolean | undefined

When an element is disabled, it may still be focusable. It works similarly to readOnly on form elements. In this case, only aria-disabled will be set.

(information)

State props

These props are returned by the state hook. You can spread them into this component (...state) or pass them separately. You can also provide these props from your own state logic.

baseId string

ID that will serve as a base for all the items IDs.

orientation "horizontal" | "vertical" | undefined

Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus:

currentId string | null | undefined

The current focused item id.

wrap boolean | "horizontal" | "vertical"

If enabled, moving to the next item from the last one in a row or column will focus the first item in the next row or column and vice-versa.

groups Group[]

Lists all the composite groups with their id and DOM ref. This state is automatically updated when registerGroup and unregisterGroup are called.

items Item[]

Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called.

move (id: string | null) => void

Moves focus to a given item ID.

setCurrentId (value: SetStateAction<string | null | undefined>) => void

Sets currentId.

first () => void

Moves focus to the first item.

last () => void

Moves focus to the last item.


id string | undefined

Same as the HTML attribute.

tabId string | undefined

Tab's id

(information)

State props

These props are returned by the state hook. You can spread them into this component (...state) or pass them separately. You can also provide these props from your own state logic.

baseId string

ID that will serve as a base for all the items IDs.

visible boolean

Whether it's visible or not.

animating boolean

Whether it's animating or not.

animated number | boolean

If true, animating will be set to true when visible is updated. It'll wait for stopAnimation to be called or a CSS transition ends. If animated is set to a number, stopAnimation will be called only after the same number of milliseconds have passed.

stopAnimation () => void

Stops animation. It's called automatically if there's a CSS transition.

selectedId string | null | undefined

The current selected tab's id.

items Item[]

Lists all the composite items with their id, DOM ref, disabled state and groupId if any. This state is automatically updated when registerItem and unregisterItem are called.

panels Item[]

Lists all the panels.

registerPanel (item: Item) => void

Registers a tab panel.

unregisterPanel (id: string) => void

Unregisters a tab panel.