Skip to content
Components, switch product

Tabs

<gui-tabs> shows one panel of content at a time, under a strip of tabs. You write the tabs and the panels; the element adds the behavior: the roles and ARIA links, the keyboard, and which panel shows. It never creates or removes your content.

Personal Address Preferences
Code of the example above
TabsBasic.tsx
import {
GuiCheckbox,
GuiTab,
GuiTabList,
GuiTabPanel,
GuiTabs,
GuiTextinput,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TabsBasic() {
return (
<GuiTabs>
<GuiTabList aria-label="Profile">
<GuiTab panel="personal">Personal</GuiTab>
<GuiTab panel="address">Address</GuiTab>
<GuiTab panel="preferences">Preferences</GuiTab>
</GuiTabList>
<GuiTabPanel name="personal">
<GuiTextinput label="Full name" value="Ada Lovelace" />
</GuiTabPanel>
<GuiTabPanel name="address">
<GuiTextinput label="City" value="London" />
</GuiTabPanel>
<GuiTabPanel name="preferences">
<GuiCheckbox label="Email me a weekly summary" />
</GuiTabPanel>
</GuiTabs>
);
}

The element’s entry point is @golemui/gui-components/tabs, which registers <gui-tabs> and its parts, <gui-tab-list>, <gui-tab> and <gui-tab-panel>, and nothing else. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

TabsExample.tsx
import { GuiTabs, GuiTabList, GuiTab, GuiTabPanel } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TabsExample() {
return (
<GuiTabs
active="address"
>
<GuiTabList aria-label="Profile">
<GuiTab panel="personal">Personal</GuiTab>
<GuiTab panel="address">Address</GuiTab>
</GuiTabList>
<GuiTabPanel name="personal">…</GuiTabPanel>
<GuiTabPanel name="address">…</GuiTabPanel>
</GuiTabs>
);
}

Each gui-tab points at its panel with panel, the name of a gui-tab-panel. Your framework renders the parts like any other markup, so loops and conditions work as usual: the element follows when you add, remove or rename a tab.

Properties

Property Type Default Description
active string – The panel of the selected tab. Defaults to the first tab.

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-tab-change The user selected a tab. detail.value is its panel. Cancel it to keep the current tab.

The first tab is selected unless you set active to the panel of another tab. An active that matches no tab selects the first one too.

Monthly Annual Team plan: $12 per seat, billed every month. Team plan: $10 per seat, billed once a year.
Code of the example above
TabsActive.tsx
import {
GuiTab,
GuiTabList,
GuiTabPanel,
GuiTabs,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TabsActive() {
return (
<GuiTabs active="annual">
<GuiTabList aria-label="Billing period">
<GuiTab panel="monthly">Monthly</GuiTab>
<GuiTab panel="annual">Annual</GuiTab>
</GuiTabList>
<GuiTabPanel name="monthly">
Team plan: $12 per seat, billed every month.
</GuiTabPanel>
<GuiTabPanel name="annual">
Team plan: $10 per seat, billed once a year.
</GuiTabPanel>
</GuiTabs>
);
}

When the user selects a tab, the element fires gui-tab-change with the tab’s panel in detail.value, then shows the panel. The event is cancelable: call preventDefault() to keep the current tab, for example while a panel has unsaved changes. Try the Settings tab below. With the arrow keys, focus still moves to the tab the user reached, while the selected tab stays.

Overview Activity Settings Analytical Engine: 12 open tasks, due 10 December. Charles Babbage closed "Punch card reader" an hour ago. Only the project owner can change these settings.

Code of the example above
TabsEvents.tsx
import { useState } from 'react';
import {
GuiTab,
GuiTabList,
GuiTabPanel,
GuiTabs,
} from '@golemui/gui-components/react';
import type { GuiTabChangeEventDetail } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
export function TabsEvents() {
const [status, setStatus] = useState('');
const onTabChange = (event: CustomEvent<GuiTabChangeEventDetail>) => {
if (event.detail.value === 'settings') {
// Keeps the current tab.
event.preventDefault();
setStatus('Ask Ada, the project owner, for access to the settings.');
return;
}
setStatus(`Showing ${event.detail.value}.`);
};
return (
<>
<GuiTabs id="project-tabs" onGuiTabChange={onTabChange}>
<GuiTabList aria-label="Project">
<GuiTab panel="overview">Overview</GuiTab>
<GuiTab panel="activity">Activity</GuiTab>
<GuiTab panel="settings">Settings</GuiTab>
</GuiTabList>
<GuiTabPanel name="overview">
Analytical Engine: 12 open tasks, due 10 December.
</GuiTabPanel>
<GuiTabPanel name="activity">
Charles Babbage closed "Punch card reader" an hour ago.
</GuiTabPanel>
<GuiTabPanel name="settings">
Only the project owner can change these settings.
</GuiTabPanel>
</GuiTabs>
<p id="project-tabs-status" role="status">
{status}
</p>
</>
);
}

Set active to select a tab yourself, such as from a “Next step” button. Setting it fires no gui-tab-change. When the user selects a tab, active holds its panel, so it is always the selected tab.

1. Cart 2. Shipping 3. Payment Two items: a notebook and a fountain pen. Ship to 12 Marylebone Road, London. Pay with the card ending in 4242.
Code of the example above
TabsControlled.tsx
import { useState } from 'react';
import {
GuiButton,
GuiTab,
GuiTabList,
GuiTabPanel,
GuiTabs,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const steps = ['cart', 'shipping', 'payment'];
export function TabsControlled() {
const [active, setActive] = useState<string>();
const next = () =>
setActive(steps[(steps.indexOf(active ?? 'cart') + 1) % steps.length]);
return (
<>
<GuiTabs
id="checkout-tabs"
active={active}
onGuiTabChange={(event) => setActive(event.detail.value)}
>
<GuiTabList aria-label="Checkout">
<GuiTab panel="cart">1. Cart</GuiTab>
<GuiTab panel="shipping">2. Shipping</GuiTab>
<GuiTab panel="payment">3. Payment</GuiTab>
</GuiTabList>
<GuiTabPanel name="cart">
Two items: a notebook and a fountain pen.
</GuiTabPanel>
<GuiTabPanel name="shipping">
Ship to 12 Marylebone Road, London.
</GuiTabPanel>
<GuiTabPanel name="payment">
Pay with the card ending in 4242.
</GuiTabPanel>
</GuiTabs>
<GuiButton id="checkout-next" label="Next step" onClick={next} />
</>
);
}

When the tabs don’t fit, the strip scrolls sideways, and a shadow marks each side with tabs out of view. The strip scrolls to the selected tab when it first shows.

January February March April May June July August September October November December January: 31 orders. February: 28 orders. March: 40 orders. April: 37 orders. May: 45 orders. June: 52 orders. July: 61 orders. August: 58 orders. September: 49 orders. October: 66 orders. November: 70 orders. December: 84 orders.
Code of the example above
TabsOverflow.tsx
import {
GuiTab,
GuiTabList,
GuiTabPanel,
GuiTabs,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TabsOverflow() {
return (
<GuiTabs active="oct">
<GuiTabList aria-label="Month">
<GuiTab panel="jan">January</GuiTab>
<GuiTab panel="feb">February</GuiTab>
<GuiTab panel="mar">March</GuiTab>
<GuiTab panel="apr">April</GuiTab>
<GuiTab panel="may">May</GuiTab>
<GuiTab panel="jun">June</GuiTab>
<GuiTab panel="jul">July</GuiTab>
<GuiTab panel="aug">August</GuiTab>
<GuiTab panel="sep">September</GuiTab>
<GuiTab panel="oct">October</GuiTab>
<GuiTab panel="nov">November</GuiTab>
<GuiTab panel="dec">December</GuiTab>
</GuiTabList>
<GuiTabPanel name="jan">January: 31 orders.</GuiTabPanel>
<GuiTabPanel name="feb">February: 28 orders.</GuiTabPanel>
<GuiTabPanel name="mar">March: 40 orders.</GuiTabPanel>
<GuiTabPanel name="apr">April: 37 orders.</GuiTabPanel>
<GuiTabPanel name="may">May: 45 orders.</GuiTabPanel>
<GuiTabPanel name="jun">June: 52 orders.</GuiTabPanel>
<GuiTabPanel name="jul">July: 61 orders.</GuiTabPanel>
<GuiTabPanel name="aug">August: 58 orders.</GuiTabPanel>
<GuiTabPanel name="sep">September: 49 orders.</GuiTabPanel>
<GuiTabPanel name="oct">October: 66 orders.</GuiTabPanel>
<GuiTabPanel name="nov">November: 70 orders.</GuiTabPanel>
<GuiTabPanel name="dec">December: 84 orders.</GuiTabPanel>
</GuiTabs>
);
}

gui-tabs hides the inactive panels when it upgrades in the browser. Until then, in a page rendered on the server, every panel shows. Put hidden on the inactive panels in your markup to keep them out of sight from the start: the element manages the attribute once it upgrades. See Server rendering.

<gui-tab-list> is the strip of tabs. It renders nothing of its own: it holds your gui-tabs, gets the tablist role and scrolls when the tabs don’t fit. Name it with aria-label when the page has more than one set of tabs. It only works inside a gui-tabs.

<gui-tab> is a tab. Its content is its label: text, or text with an icon. panel names the panel it shows. It only works inside a gui-tabs, which gives it its id, its state and its keyboard.

Properties

Property Type Default Description
panel string – The name of the gui-tab-panel the tab shows.

<gui-tab-panel> holds the content of a tab. gui-tabs only hides it while its tab isn’t selected: its content stays in the page, so the fields in a hidden panel keep their values. To render a panel’s content only while it shows, leave it out yourself when its tab isn’t selected, from your framework’s state.

Properties

Property Type Default Description
name string – The name the panel's gui-tab points to with its panel.

<gui-tabs> and its parts render nothing of their own: the anatomy is your markup, with the roles, ids and states the element adds:

gui-tabs
<gui-tabs>
<gui-tab-list aria-label="Profile" role="tablist">
<gui-tab
panel="personal"
role="tab"
id="gui-tab-3"
aria-selected="true"
aria-controls="gui-tab-panel-1"
tabindex="0"
>Personal</gui-tab
>
<gui-tab
panel="address"
role="tab"
id="gui-tab-4"
aria-selected="false"
aria-controls="gui-tab-panel-2"
tabindex="-1"
>Address</gui-tab
>
</gui-tab-list>
<gui-tab-panel
name="personal"
role="tabpanel"
id="gui-tab-panel-1"
aria-labelledby="gui-tab-3"
tabindex="0"
>
…
</gui-tab-panel>
<gui-tab-panel
name="address"
role="tabpanel"
id="gui-tab-panel-2"
aria-labelledby="gui-tab-4"
tabindex="0"
hidden
>
…
</gui-tab-panel>
</gui-tabs>

The parts have no classes. Style them by tag, and by the states the element sets:

SelectorPart
gui-tabsThe whole set.
gui-tabs[overflow-start]The set while tabs are scrolled out of view at the start: shows the shadow there.
gui-tabs[overflow-end]The set while tabs are out of view at the end: shows the shadow there.
gui-tab-listThe strip of tabs.
gui-tabA tab.
gui-tab[aria-selected='true']The selected tab.
gui-tab-panelA panel.
gui-tab-panel[hidden]A panel whose tab isn’t selected.

The ids are generated, such as gui-tab-3, unless your parts have ids of their own, which the element keeps. The overflow-start and overflow-end attributes appear on gui-tabs only while tabs are out of view.

The tabs read these design tokens. Set them on :root to change every set of tabs, or on any element to change the tabs inside it:

TokenWhat it styles
--gui-bg-defaultThe background of the tabs and panels, and of the scroll shadows.
--gui-border-defaultThe borders of the tabs and panels.
--gui-text-defaultThe text, and the outline of the focused tab.
--gui-radius-mdThe corners of the tabs and panels.
--gui-space-3The padding of the tabs and panels, and the space around the set.
--gui-widget-sizeThe height of the scroll shadows.
--gui-font-familyThe font.
--gui-font-smThe text size.
--gui-space-2The padding of the tabs in a narrow container.
--gui-font-xsThe text size of the tabs in a narrow container.

In a narrow container, below 480px wide, the tabs take less room.

styles.css
/* Panels on a raised surface, with rounder tabs. */
.settings {
--gui-bg-default: #fbfaf7;
--gui-radius-md: 12px;
}

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Roles. The strip has the tablist role, each tab the tab role and each panel the tabpanel role. A part you give a role of your own keeps it.
  • Links. Each tab points at its panel with aria-controls, and each panel is named by its tab through aria-labelledby.
  • State. The selected tab has aria-selected="true"; the others false.
  • Name. Name the strip with aria-label on gui-tab-list, such as “Profile”, when the page has more than one set of tabs.
  • Focus. Only the selected tab is in the tab order. Each panel is focusable too, so keyboard users reach a panel with no focusable content inside; give it tabindex="-1" when its first element is focusable.
KeyAction
TabMoves focus to the selected tab, then into its panel.
→Selects the next tab and moves focus to it.
←Selects the previous tab and moves focus to it.
HomeSelects the first tab.
EndSelects the last tab.

In a right-to-left page, → and ← swap. The arrows stop at the first and last tabs.

What every element shares is in Accessibility.