Eunomia v1.1.0
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-labelstring—Accessible 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
fullbooleanfalseStretches the tab header to 100% of the group's width, dividing it equally between the tabs and centering each label. Makes align irrelevant. Combines with overflow scrolling : a tab whose label doesn't fit its share keeps the room it needs
fillbooleanfalseMakes the active panel fill the height left under the tab header, so its content can scroll inside it
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`
hide-indicatorbooleanfalseIndicates 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
panelstring—The name of the panel associated with this tab
activebooleanfalseIndicates whether the tab is currently active (selected)
iconEunomiaIconName | undefined—The 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.

Properties

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

NameTypeDefaultDescription
hostTabStop—Whether the host itself is a tab stop while enabled. `false` for a component whose focusable controls live inside its shadow root.

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-focus-background-colorSet a custom background color for the tab in case of keyboard focus
--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-padding-blockSets the vertical padding, as one length applied to both sides. Ignored when --tab-padding is set
--tab-padding-inlineSets the horizontal padding, as one length applied to both sides. Ignored when --tab-padding is set
--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-focus-outline-widthSet a custom width to the focus outline
--tab-focus-outline-offsetSet a custom offset to the focus outline
--tab-font-sizeSet a custom font size to the tab label
--tab-font-weightSet a custom font weight to the tab label
--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
namestring—The 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

CSS custom properties

NameDescription
--tab-panel-font-sizeSet a custom font size to the panel content
--tab-panel-focus-outlineSet a custom color to the focus outline, shown when the panel itself takes focus
--tab-panel-focus-outline-widthSet a custom width to the focus outline
--tab-panel-focus-outline-offsetSet a custom offset to the focus outline

Layout and display

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>

Full width

Set full to stretch the tab header to 100% of the group's width. The width is divided equally between the tabs, and each label is centered in its share. It makes align irrelevant, since nothing is left to align. A tab whose label doesn't fit its share keeps the room it needs, and the header scrolls like in "Overflow scrolling" below once the labels no longer fit.

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

Filling the height

Set fill when the group has a height of its own, such as a grid row or a fixed height, and the active panel should take all the room left under the tab header. Give the panel overflow: auto so its content scrolls inside it while the tabs stay in place.

First Second

First panel, long enough to scroll.

Line 2

Line 3

Line 4

Line 5

Line 6

Line 7

Second panel.
<eun-tab-group fill style="height: 100%;">
  ...
  <eun-tab-panel name="a" style="overflow: auto;"> ... </eun-tab-panel>
</eun-tab-group>

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>

Behavior

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.

Content

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
>

Server-side rendering

Before the tab group upgrades, every panel shows one under the other, then all but one disappear. Link tabs-ssr.css in the page's <head> to show the tab list and the active panel only, from the first paint:

<link
  rel="stylesheet"
  href="node_modules/@eunomia/elements/dist/ssr/tabs-ssr.css"
/>

A bundler that resolves package exports can reach it as @eunomia/elements/tabs-ssr.css.

The active panel is the one at the same position as the tab marked active, or the first one, so keep the panels in the same order as the tabs (up to 12). It covers align, full, hide-indicator, disabled tabs and the --tab-* spacing and color properties. Tab icons appear with the upgrade, and a tab list too long for its width wraps until then instead of scrolling.

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>

The custom properties that change colors, borders, shadows and corners, at a glance. The API tab lists every property of the component.

Variable Sets
--tab-background-color Set a custom background color for the tab
--tab-hover-background-color Set a custom background color for the tab in case of hover state
--tab-active-background-color Set a custom background color for the tab in case of active state
--tab-focus-background-color Set a custom background color for the tab in case of keyboard focus
--tab-text-color Set a custom color for the tab label
--tab-hover-text-color Set a custom color to the tab label in case of hover state
--tab-active-text-color Srt a custom color to the tab label in case of active state
--tab-border Set a custom border to the tab.
--tab-border-active Set a custom border to the tab on active state.
--tab-border-radius Set a custom border radius to the tab.
--tab-focus-outline Set a custom color to the focus outline
--tab-focus-outline-width Set a custom width to the focus outline
--tab-panel-focus-outline Set a custom color to the panel's focus outline, shown when the panel itself takes focus
--tab-notification-color Set a custom color to the notification badge
--tab-header-background Set a custom color to the background of the tab header
--tab-indicator-color Set a custom color to the indicator positionned under the active tab
--tab-hover-indicator-color Set a custom color to the indicator positionned under the hover tab
--tab-divider-color Set a custom color to the divider under the tab header
--tab-arrows-color Set a custom background color to the tab arrow navigation in case of overflowing component
--tab-panel-focus-outline-width Set a custom width to the focus outline

Keyboard and focus

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.

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. It then draws its own focus ring, set with --tab-panel-focus-outline.

The eun-tab host is the element that takes focus, but its ring is drawn on the tab by the component itself and the host's own outline is forced to none. A global *:focus-visible rule on the page therefore never adds a second ring around a 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.

Semantics and screen readers

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.

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.