Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Tabs

Tabs are made of three pieces working together: a group container, one clickable header per view, and the content panel shown for whichever header is currently active. This exact trio powers the tabs at the top of this very page.

Dependencies

eun-icon · for a tab's own icon, or the overflow scroll arrows
Overview API Examples Accessibility
Overview Details Settings Overview content. Details content. Settings content.

When to use

Use tabs to let someone switch between a few different views of related content that all live on the same page, without navigating away. This very page's own Overview, API, Examples, and Accessibility split is a tabs component. For a sequence of steps someone has to complete in order, such as a checkout or a multi-step form, a stepper fits better than tabs.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/tabs.js";
<eun-tab-group>
  <eun-tab slot="navigation" panel="overview">Overview</eun-tab>
  <eun-tab slot="navigation" panel="details">Details</eun-tab>

  <eun-tab-panel name="overview">Overview content.</eun-tab-panel>
  <eun-tab-panel name="details">Details content.</eun-tab-panel>
</eun-tab-group>

Importing the file registers all three elements at once. Each eun-tab's panel attribute must match the name of the eun-tab-panel it controls. The group wires them together itself, with nothing to configure on the panels.

Alternatives

You want .. Prefers A sequence of steps someone has to complete in order, such as a checkout or a multi-step form eun-stepper

Guidance

  • Use tabs to switch between views of related content on the same page
  • Keep labels short: one or two words
  • Give each eun-tab a unique panel matching one eun-tab-panel's name
  • Using tabs for a multi-step, sequential process: see Alternatives above
  • Nesting a tab group's panels inside another tab group's panel of the same instance
  • Hiding a validation error inside an inactive tab without any visible indicator

Live testing

Properties

Tab group <eun-tab-group>

Attributes

NameTypeDefaultDescription
aria-labelstringAccessible name for the tablist, forwarded to the internal `role="tablist"` element. Recommended when the group's purpose isn't already clear from a preceding heading, or when several tab groups appear on the same page
align'start' | 'center' | 'end'"start"The tabs position alignment according to the component
animation'fade' | 'slide' | 'none'"slide"The animation to apply to a click/keyboard-triggered panel switch ; a touch swipe's own release settling into place is never this, see the class doc and `--tab-panel-swipe-duration`
hideIndicatorbooleanfalseIndicates whether the indicators under a tab should be displayed or not

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
overflowStatebooleanfalseThe current overflow position of the scrollable element.

Slots

NameDescription
(default)The tab content corresponding to the `tab-panel` component
navigationThe tab header corresponding to the `tab` component

Events

NameTypeDescription
tabchangeTabChangeEventEmitted event on active panel change (click, keyboard, or a committed touch swipe alike)

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--tab-header-backgroundSet a custom color to the background of the tab header
--tab-indicator-colorSet a custom color to the indicator positionned under the active tab
--tab-hover-indicator-colorSet a custom color to the indicator positionned under the hover tab
--tab-indicator-heightSet a custom height to the indicator positionned under the active tab
--tab-divider-colorSet a custom color to the divider under the tab header
--tab-divider-sizeSet a custom thickness to the divider under the tab header
--tab-gapSet a custom gap between each tabs of the tab group
--tab-panel-gapSet a custom gap between the tab header and the panel
--tab-divider-gapSet a custom gap between the tab divider and the tabs
--tab-arrows-colorSet a custom background color to the tab arrow navigation in case of overflowing component
--tab-panel-animationCustom animation time for the tab panel switch (click/keyboard only)
--tab-panel-swipe-durationCustom duration for a touch swipe's release settling into place (committing to the new panel, or springing back) ; forced to `0ms` under `prefers-reduced-motion: reduce` or `animation="none"`, regardless of this

Tab <eun-tab>

Attributes

NameTypeDefaultDescription
panelstringThe name of the panel associated with this tab
activebooleanfalseIndicates whether the tab is currently active (selected)
iconEunomiaIconName | undefinedThe name of the Eunomia icon to display inside the tab
badgebooleanfalseIndicates whether a badge (notification indicator) should be displayed

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
(default)The label of the tab
iconUsed to set a custom icon different from Eunomia one
badgePositions slotted content (typically a `eun-badge`) into the same spot the boolean `badge` dot indicator occupies

CSS custom properties

NameDescription
--tab-background-colorSet a custom background color for the tab
--tab-hover-background-colorSet a custom background color for the tab in case of hover state
--tab-active-background-colorSet a custom background color for the tab in case of active state
--tab-text-colorSet a custom color for the tab label
--tab-hover-text-colorSet a custom color to the tab label in case of hover state
--tab-active-text-colorSrt a custom color to the tab label in case of active state
--tab-paddingSet a custom padding into the tab.
--tab-borderSet a custom border to the tab.
--tab-border-activeSet a custom border to the tab on active state.
--tab-border-radiusSet a custom border radius to the tab.
--tab-focus-outlineSet a custom color to the focus outline
--tab-notification-colorSet a custom color to the notification badge
--tab-disabled-displaySet a custom `display` for a disabled tab, e.g. `none` to hide it entirely instead of showing it dimmed

Tab panel <eun-tab-panel>

Attributes

NameTypeDefaultDescription
namestringThe name of the panel
is-hiddenbooleantrueIndicates whether the panel is visible or not

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
(default)The panel content

Alignment

align on eun-tab-group positions the tab headers: start (default), center, or end.

start (default)

First Second First panel. Second panel.
<eun-tab-group> ... </eun-tab-group>

center

First Second First panel. Second panel.
<eun-tab-group align="center"> ... </eun-tab-group>

end

First Second First panel. Second panel.
<eun-tab-group align="end"> ... </eun-tab-group>

Animation

animation controls the panel-switch transition for a click or keyboard activation: slide (default), fade, or none. Click between the tabs below to compare. A touch swipe is never affected by this setting, so see Touch swipe below.

slide (default)

First Second First panel. Second panel.

fade

First Second First panel. Second panel.

none

First Second First panel. Second panel.
<eun-tab-group animation="fade"> ... </eun-tab-group>
<eun-tab-group animation="none"> ... </eun-tab-group>

Touch swipe

On a touch device, dragging horizontally anywhere over the active panel pages to the next/previous tab. The panel and its incoming neighbor track the finger 1:1 the whole way, not just a jump once the gesture ends, and release past the swipe threshold to commit or short of it to spring back, easing the short remaining distance into place either way. That settle is a plain position transition, not animation (see Animation above), which never applies to a swipe, only to a click or keyboard change. This works out of the box, with nothing to configure. Try it on the demo below with a touchscreen or your browser's device emulation.

First Second Third First panel, swipe left. Second panel, swipe either way. Third panel, swipe right.

Disabled tabs are skipped, same as ArrowLeft/ArrowRight, and dragging past the first/last enabled tab meets resistance instead of doing nothing. See the mobile swipe test page for a full-width harness to try this on an actual phone.

Icons and badges

A eun-tab accepts an icon attribute (a Eunomia icon name) or a custom icon in the icon slot, plus a boolean badge to show a notification dot.

Inbox Sent Inbox content. Sent content.
import "@eunomia/elements/icon.js";
<eun-tab slot="navigation" panel="inbox" icon="mail" badge> Inbox </eun-tab>

The icon module must be imported alongside the tabs for the icon to render.

Numeric badge

For a real count instead of a plain dot, slot a eun-badge into the tab's own badge slot. It lands in the exact same spot the boolean dot above occupies, and takes priority over it if both are somehow set at once.

Inbox 5 Sent Inbox content. Sent content.
import "@eunomia/elements/icon.js";
import "@eunomia/elements/badge.js";
<eun-tab slot="navigation" panel="inbox" icon="mail">
  Inbox
  <eun-badge slot="badge">5</eun-badge>
</eun-tab>

Disabled tab

A disabled tab can't be activated, by click or keyboard, and is skipped when navigating with arrow keys.

Available Disabled Available content. Unreachable content.
<eun-tab slot="navigation" panel="b" disabled>Disabled</eun-tab>

Set --tab-disabled-display: none; to hide a disabled tab entirely instead of showing it dimmed out, for cases where a permission or feature-flag check should remove the option rather than just block it.

Available Hidden Also available Available content. Unreachable content. Also available content.
<eun-tab
  slot="navigation"
  panel="b"
  disabled
  style="--tab-disabled-display: none;"
  >Hidden</eun-tab
>

Overflow scrolling

When the tab headers are wider than the available space, the header row becomes horizontally scrollable and left/right arrows appear to scroll it, rather than wrapping or shrinking the tabs. This is especially common on mobile viewports.

Overview Details Settings Members Billing Integrations Overview content. Details content. Settings content. Members content. Billing content. Integrations content.
<eun-tab-group style="max-width: 320px; --tab-arrows-color: #f4f4f2;">
  ...
</eun-tab-group>

--tab-arrows-color defaults to --eun-color-grey-0 (plain white), which only blends in on a white surface. Set it to whatever background the tabs actually sit on, such as this page's own demo-block background, so the gradient fades into it instead of leaving a visible seam.

The demo above is capped at 320px to reproduce a mobile-width overflow regardless of your actual viewport. The same behavior kicks in on any real viewport once the tabs no longer fit.

Hiding the indicator

Set the boolean hideIndicator on eun-tab-group to remove the moving underline beneath the active tab.

<eun-tab-group hide-indicator> ... </eun-tab-group>

Theming

Every color and spacing value is exposed as a CSS custom property on eun-tab-group and eun-tab (full list in the API tab), so the tabs repaint with the active theme automatically. See Theming.

Custom Theme First panel. Second panel.
<eun-tab-group
  style="--tab-indicator-color: #b45309; --tab-active-text-color: #b45309;"
>
  ...
</eun-tab-group>

Keyboard interactions

When focus is on a tab:

Key Action
ArrowRight Moves focus to the next tab (wraps to the first)
ArrowLeft Moves focus to the previous tab (wraps to the last)
Home Moves focus to the first tab
End Moves focus to the last tab
Enter / Space Activates the focused tab

Disabled tabs are skipped entirely when navigating with arrow keys.

Aria roles

eun-tab-group exposes role="tablist" on its header row, eun-tab sets role="tab" with aria-selected and aria-controls kept in sync with the active state, and eun-tab-panel sets role="tabpanel" with aria-labelledby pointing back at its tab, all wired up automatically from the panel / name pairing, with nothing to set by hand.

Labeling the tablist

When a page has more than one tab group, or the group's purpose isn't already obvious from a preceding heading, give it an accessible name so screen reader users can tell tab groups apart:

<eun-tab-group aria-label="Account settings"> ... </eun-tab-group>

aria-label set on <eun-tab-group> is forwarded to the internal role="tablist" element. It isn't visible, only announced.

Disabled tabs

A disabled eun-tab gets aria-disabled="true" in addition to being skipped by arrow-key navigation, so assistive technology announces its unavailable state instead of only relying on the dimmed style. --tab-disabled-display: none; (see the Examples tab) removes it visually and from the tab order entirely, rather than announcing it as unavailable, for cases like a permission check where the option shouldn't appear to exist at all.

Focus management

Only the active tab is in the tab order (tabindex="0"). Inactive tabs are reachable with arrow keys but skipped by Tab, matching the standard ARIA tabs pattern. eun-tab-panel also gets tabindex="0" by default, so a panel whose content has no focusable element of its own (plain text, an image, ...) is still reachable by keyboard right after its tab.

Touch swipe

Dragging left/right anywhere over the active panel is only recognized as a swipe once the drag is more horizontal than vertical, so a vertical drag still scrolls the page normally. It activates the same next/previous tab ArrowRight/ArrowLeft would, tracking the finger live rather than only reacting once the gesture ends, then easing the short remaining distance into place on release (committing, or springing back). It's purely an additive touch convenience: every tab stays present and independently reachable regardless of pointer type, and click/keyboard activation are unchanged. Disabled tabs are skipped, same as arrow-key navigation.

Reduced motion

The panel-switch animation (slide / fade), the moving active-tab / hover indicators, and a touch swipe's own release-settling transition all respect prefers-reduced-motion: reduce: with that OS-level setting on, every transition and animation is disabled regardless of the animation attribute, so a click/keyboard panel switch, and a swipe's release, happen instantly instead of easing. A swipe's live 1:1 tracking while the finger is still moving is unaffected either way, since that's direct manipulation rather than a decorative animation. See Touch swipe above.