sdxc

Type to search, or start from one of these:

Tabs

A set of tabs whose active view comes from routing rather than tracked selection state.

Public profile#profile
Your name, avatar and bio as everyone else sees them.
View code
// The active tab comes from the URL, never from tracked state.
let activeIndex = sections.findIndex((section) => section.fragment === fragment);
let active = sections[activeIndex];

<Tabs>
	<Tabs.List aria-label="Settings sections" activeIndex={activeIndex} tabSize="7rem">
		{sections.map((section, index) => (
			<Tabs.Tab
				key={section.fragment}
				href={section.fragment}
				aria-selected={index === activeIndex}
				mix={[
					is("7rem"),
					// A fragment navigation leaves no history entry the runtime reports,
					// so the island reads the URL back on the press itself.
					on<HTMLAnchorElement, "click">("click", () => {
						fragment = section.fragment;
						void handle.update();
					}),
				]}
			>
				{section.label}
			</Tabs.Tab>
		))}
	</Tabs.List>
	<Tabs.Panels>
		<Tabs.Panel aria-label={active.heading} mix={[vstack({ gap: 2, align: "start" })]}>
			<div mix={[hstack({ gap: 2, align: "center" })]}>
				<span mix={[text("base"), weight("semibold")]}>{active.heading}</span>
				<Badge variant="secondary">{active.fragment}</Badge>
			</div>
			<Text>{active.body}</Text>
		</Tabs.Panel>
	</Tabs.Panels>
</Tabs>

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

npm add @sdxc/ui
import "@sdxc/ui/theme.css";

Usage

import { Tabs } from "@sdxc/ui";

Renders the root host: a plain <div> stacking Tabs.List above Tabs.Panels, switching to a side-by-side row when orientation is "vertical" and sharing that axis through component context.

Composition

The parts the component publishes, each a static property of the host.

  • Tabs
  • Tabs.ListRenders the tab strip: a role="tablist" host laying Tabs.Tab links out in a row, or a column when orientation is "vertical", with a sliding accent bar reading --ui-tab-indicator-* custom properties.
  • Tabs.TabRenders a single tab: a native <a> carrying the tab role, its own border edge kept transparent to reserve space for the sliding indicator.
  • Tabs.PanelsRenders the panel region: a <div> growing to fill whatever space is left beside or beneath Tabs.List.
  • Tabs.PanelRenders the current tab's content: a padded <div> carrying the tabpanel role, focusable by default so a keyboard user can jump straight into it after activating a tab, with a focus-visible ring.

Examples

<Tabs orientation="vertical">
	<Tabs.List aria-label={t("settings.sections")}>
		<Tabs.Tab href="/settings/profile" aria-selected="true">{t("settings.profile")}</Tabs.Tab>
		<Tabs.Tab href="/settings/billing" aria-selected="false">{t("settings.billing")}</Tabs.Tab>
	</Tabs.List>
	<Tabs.Panels>
		<Tabs.Panel>{t("settings.profile.body")}</Tabs.Panel>
	</Tabs.Panels>
</Tabs>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeDescription
orientation?"horizontal" | "vertical"Layout axis. Defaults to DEFAULT_ORIENTATION.

Also accepts everything in TagProps<"div">.

Tabs.List

PropTypeDescription
activeIndex?numberIndex of the currently active tab, for a route-driven tab bar with no client-side measurement of the active tab's bounds. Given with tabSize, Tabs.List computes its own indicator position.
tabSize?SizeValueEvery Tabs.Tab's fixed size along the enclosing Tabs's layout axis, required alongside activeIndex since the indicator's offset depends on how far apart the tabs sit.

Also accepts everything in TagProps<"div">.

Tabs.Tab

Tabs.Panels

Also accepts everything in TagProps<"div">.

Tabs.Panel

Also accepts everything in TagProps<"div">.

Types

NameValuesWhat it decides
Orientation"horizontal" | "vertical"Axis Tabs.List's links lay out along: a single row with the active indicator running beneath it, or a single column with the indicator running alongside it.

Other components this module publishes.

  • tabIndicatorMix