Skip to content

OTabs ​

OTabs renders accessible tabs as pills grouped in one track, the selected one raised on a filled background, or a bottom bar with an accent indicator within thumb reach on mobile, driven by v-model so an application can bind them to its URL. Each tab is an OTabPane child that holds its own content, such as a component.

Usage ​

Step by step ​

  1. Import OTabs and OTabPane from the ooko-ui/tabs public subpath.
  2. Bind the selected value with v-model and name the list with aria-label.
  3. Add one OTabPane per tab with a unique value and a label, and put the tab's content, such as a component, in its default slot. Mark a pane lazy to mount its content only while it is selected, or lazy-once to mount it the first time it is selected and keep it afterwards.
  4. Bind v-model to a route query or path to keep the selection in the URL; OTabs never uses a router.

Example ​

The rendered example and its source come from the same Vue single-file component.

Content of the overview tab.

Interactive playground ​

Change the selected tab and the variant, and disable a tab to see keyboard navigation skip it.

Content of the overview tab.

Props ​

PropTypeDefaultDescription
modelValuestringundefinedValue of the selected tab.
variantOTabsVariant"line"Underlined tabs above the panel, or a bottom bar below it within thumb reach on mobile.
ariaLabelstringundefinedAccessible name of the tab list.

Events ​

EventPayloadDescription
update:modelValuestringRequests a new selected tab.

Slots ​

SlotPropsDescription
defaultnoneOTabPane children, one per tab, in display order.
TypeDefinition
OTabsVarianttype OTabsVariant = "line" | "bottom-bar";
OTabPanePropsinterface OTabPaneProps { /** Unique value bound to the parent OTabsv-model when the tab is selected. */ value: string; /** Visible text and accessible name of the tab. */ label: string; /** Prevents selecting the tab and skips it during keyboard navigation. */ disabled?: boolean; /** Mounts the content only while the tab is selected, and unmounts it when another tab is selected. */ lazy?: boolean; /** Mounts the content the first time the tab is selected, then keeps it mounted and hidden. */ lazyOnce?: boolean; }
OTabPaneSlotsinterface OTabPaneSlots { /** Content of the panel; without it, the tab has no panel. */ default?: () => unknown; }

CSS custom properties ​

Custom propertyDefaultDescription
--o-tabs-track-backgroundvar(--o-background-track)Sets the background of the track that groups the tabs.
--o-tabs-track-border1px solid var(--o-border-color-subtle)Sets the border shorthand of the track that groups the tabs.
--o-tabs-track-paddingvar(--o-space-1)Sets the space between the track edge and its tabs.
--o-tabs-selected-backgroundvar(--o-background-track-thumb)Sets the background of the selected tab pill, raised above the track.
--o-tabs-selected-border-colorvar(--o-border-color-subtle)Sets the border color of the selected tab pill.
--o-tabs-tab-border-radiusvar(--o-radius-round)Sets the corner radius of each tab.
--o-tabs-gapvar(--o-space-0-5)Sets the space between tabs.
--o-tabs-indicator-colorvar(--o-border-primary-default)Sets the accent color of the selected bottom-bar tab indicator.
--o-tabs-indicator-thickness2pxSets the thickness of the selected bottom-bar tab indicator.
--o-tabs-tab-paddingvar(--o-space-1) var(--o-space-4)Sets the padding inside each tab.
--o-tabs-bottom-bar-height3.5remSets the minimum height of bottom-bar tabs, sized for touch.

Override example ​

css
:root {
  --o-tabs-selected-background: var(--o-background-primary-subtle);
  --o-tabs-indicator-color: var(--o-border-secondary-default);
}

Accessibility ​

  • The list uses the tablist role and each tab is a native button with the tab role and aria-selected. Name the list with aria-label.
  • Each pane with content renders a panel with the tabpanel role, labelled by its tab and referenced by the tab's aria-controls. Panels of unselected tabs are hidden. The visible panel is focusable so keyboard users can reach content without controls.
  • Only one tab is in the Tab sequence (roving tabindex). Left and Right arrows move to the previous or next enabled tab and wrap; Home and End move to the first and last enabled tab. Arrow directions follow the writing direction in right-to-left layouts.
  • Selection follows focus: moving with the keyboard requests the newly focused tab.
  • Disabled tabs use the native disabled attribute and are skipped by keyboard navigation.
  • The selected tab is marked by its filled pill and border (the indicator bar in the bottom bar), not by color alone, and forced-colors mode draws that border with the system highlight color. Focus receives a visible outline.

Behavior and edge cases ​

  • The selection is controlled: OTabs emits update:modelValue and renders the change only once the parent updates modelValue. Selecting the current or a disabled tab emits nothing.
  • When modelValue matches no enabled tab, the first enabled tab receives keyboard focus and no panel is visible until a valid value is bound.
  • By default every pane's content stays mounted and unselected panels are hidden, so components keep their local state. lazy unmounts the content when another tab is selected; lazy-once defers mounting until the first selection. The selected pane always renders, including during SSR.
  • OTabs reads its OTabPane children directly, including those produced by v-for and v-if; other children are ignored. Give each pane a unique value.
  • A pane without a default slot renders no panel; use this when the content lives elsewhere, such as a RouterView.
  • The bottom-bar variant renders the panels before the list, stretches tabs to equal widths, sticks the list to the bottom of its scrolling container, and pads it for the device safe area. Give the parent a height to keep the bar at the bottom when the content is short.
  • The line variant scrolls the list horizontally when tabs overflow.
  • Native attributes and listeners are forwarded to the root element.