Tabs

A set of layered sections of content, known as tab panels, that display one panel of content at a time.

Manage your account details and preferences.

View source
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsHero() {
  return (
    <Tabs.Root defaultValue="account" class={styles.root}>
      <Tabs.List class={styles.list}>
        <Tabs.Trigger value="account" class={styles.trigger}>
          Account
        </Tabs.Trigger>
        <Tabs.Trigger value="password" class={styles.trigger}>
          Password
        </Tabs.Trigger>
        <Tabs.Trigger value="settings" class={styles.trigger}>
          Settings
        </Tabs.Trigger>
        <Tabs.Indicator class={styles.indicator} />
      </Tabs.List>
      <Tabs.Content value="account" class={styles.content}>
        <p>Manage your account details and preferences.</p>
      </Tabs.Content>
      <Tabs.Content value="password" class={styles.content}>
        <p>Change your password and security settings.</p>
      </Tabs.Content>
      <Tabs.Content value="settings" class={styles.content}>
        <p>Configure your application settings.</p>
      </Tabs.Content>
    </Tabs.Root>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Examples

Default value

Use defaultValue to set the initially active tab without controlling the state.

Change your password and security settings.

View source
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsDefaultValue() {
  return (
    <Tabs.Root defaultValue="password" class={styles.root}>
      <Tabs.List class={styles.list}>
        <Tabs.Trigger value="account" class={styles.trigger}>
          Account
        </Tabs.Trigger>
        <Tabs.Trigger value="password" class={styles.trigger}>
          Password
        </Tabs.Trigger>
        <Tabs.Trigger value="settings" class={styles.trigger}>
          Settings
        </Tabs.Trigger>
        <Tabs.Indicator class={styles.indicator} />
      </Tabs.List>
      <Tabs.Content value="account" class={styles.content}>
        <p>Manage your account details and preferences.</p>
      </Tabs.Content>
      <Tabs.Content value="password" class={styles.content}>
        <p>Change your password and security settings.</p>
      </Tabs.Content>
      <Tabs.Content value="settings" class={styles.content}>
        <p>Configure your application settings.</p>
      </Tabs.Content>
    </Tabs.Root>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Controlled

Use value and onChange to control the active tab externally.

Manage your account details and preferences.

Active tab: account

View source
import { createSignal } from "solid-js"
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsControlled() {
  const [tab, setTab] = createSignal("account")

  return (
    <div>
      <Tabs.Root value={tab()} onChange={setTab} class={styles.root}>
        <Tabs.List class={styles.list}>
          <Tabs.Trigger value="account" class={styles.trigger}>
            Account
          </Tabs.Trigger>
          <Tabs.Trigger value="password" class={styles.trigger}>
            Password
          </Tabs.Trigger>
          <Tabs.Trigger value="settings" class={styles.trigger}>
            Settings
          </Tabs.Trigger>
          <Tabs.Indicator class={styles.indicator} />
        </Tabs.List>
        <Tabs.Content value="account" class={styles.content}>
          <p>Manage your account details and preferences.</p>
        </Tabs.Content>
        <Tabs.Content value="password" class={styles.content}>
          <p>Change your password and security settings.</p>
        </Tabs.Content>
        <Tabs.Content value="settings" class={styles.content}>
          <p>Configure your application settings.</p>
        </Tabs.Content>
      </Tabs.Root>
      <p class={styles.status}>Active tab: {tab()}</p>
    </div>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Manual activation

Use activationMode="manual" so keyboard navigation moves focus without activating the tab — the user must press Enter or Space to select.

Manage your account details and preferences.

View source
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsManualActivation() {
  return (
    <Tabs.Root defaultValue="account" activationMode="manual" class={styles.root}>
      <Tabs.List class={styles.list}>
        <Tabs.Trigger value="account" class={styles.trigger}>
          Account
        </Tabs.Trigger>
        <Tabs.Trigger value="password" class={styles.trigger}>
          Password
        </Tabs.Trigger>
        <Tabs.Trigger value="settings" class={styles.trigger}>
          Settings
        </Tabs.Trigger>
        <Tabs.Indicator class={styles.indicator} />
      </Tabs.List>
      <Tabs.Content value="account" class={styles.content}>
        <p>Manage your account details and preferences.</p>
      </Tabs.Content>
      <Tabs.Content value="password" class={styles.content}>
        <p>Change your password and security settings.</p>
      </Tabs.Content>
      <Tabs.Content value="settings" class={styles.content}>
        <p>Configure your application settings.</p>
      </Tabs.Content>
    </Tabs.Root>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Vertical

Use orientation="vertical" to render tabs in a vertical layout.

Manage your account details and preferences.

View source
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsVertical() {
  return (
    <Tabs.Root defaultValue="account" orientation="vertical" class={styles.rootVertical}>
      <Tabs.List class={styles.listVertical}>
        <Tabs.Trigger value="account" class={styles.triggerVertical}>
          Account
        </Tabs.Trigger>
        <Tabs.Trigger value="password" class={styles.triggerVertical}>
          Password
        </Tabs.Trigger>
        <Tabs.Trigger value="settings" class={styles.triggerVertical}>
          Settings
        </Tabs.Trigger>
        <Tabs.Indicator class={styles.indicatorVertical} />
      </Tabs.List>
      <Tabs.Content value="account" class={styles.contentVertical}>
        <p>Manage your account details and preferences.</p>
      </Tabs.Content>
      <Tabs.Content value="password" class={styles.contentVertical}>
        <p>Change your password and security settings.</p>
      </Tabs.Content>
      <Tabs.Content value="settings" class={styles.contentVertical}>
        <p>Configure your application settings.</p>
      </Tabs.Content>
    </Tabs.Root>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Disabled tab

Use the disabled prop on an individual Tabs.Trigger to disable it.

Manage your account details and preferences.

View source
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsDisabled() {
  return (
    <Tabs.Root defaultValue="account" class={styles.root}>
      <Tabs.List class={styles.list}>
        <Tabs.Trigger value="account" class={styles.trigger}>
          Account
        </Tabs.Trigger>
        <Tabs.Trigger value="password" class={styles.trigger} disabled>
          Password
        </Tabs.Trigger>
        <Tabs.Trigger value="settings" class={styles.trigger}>
          Settings
        </Tabs.Trigger>
        <Tabs.Indicator class={styles.indicator} />
      </Tabs.List>
      <Tabs.Content value="account" class={styles.content}>
        <p>Manage your account details and preferences.</p>
      </Tabs.Content>
      <Tabs.Content value="password" class={styles.content}>
        <p>Change your password and security settings.</p>
      </Tabs.Content>
      <Tabs.Content value="settings" class={styles.content}>
        <p>Configure your application settings.</p>
      </Tabs.Content>
    </Tabs.Root>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Dynamic tabs

Drive the list of tabs from a signal to add or remove tabs at runtime.

Content for Account.

View source
import { createSignal, For } from "solid-js"
import * as Tabs from "@danielfrg/solid-ui/tabs"
import styles from "./index.module.css"

export function DemoTabsDynamic() {
  const [tabs, setTabs] = createSignal(["Account", "Password", "Settings"])
  const [selected, setSelected] = createSignal("Account")

  const addTab = () => {
    const name = `Tab ${tabs().length + 1}`
    setTabs((t) => [...t, name])
    setSelected(name)
  }

  const removeTab = () => {
    if (tabs().length <= 1) return
    const next = tabs().filter((t) => t !== selected())
    setTabs(next)
    setSelected(next[next.length - 1])
  }

  return (
    <div class={styles.dynamicWrapper}>
      <div class={styles.dynamicControls}>
        <button class={styles.dynamicButton} onClick={addTab}>
          Add tab
        </button>
        <button class={styles.dynamicButton} onClick={removeTab} disabled={tabs().length <= 1}>
          Remove tab
        </button>
      </div>
      <Tabs.Root value={selected()} onChange={setSelected} class={styles.root}>
        <Tabs.List class={styles.list}>
          <For each={tabs()}>
            {(tab) => (
              <Tabs.Trigger value={tab} class={styles.trigger}>
                {tab}
              </Tabs.Trigger>
            )}
          </For>
          <Tabs.Indicator class={styles.indicator} />
        </Tabs.List>
        <For each={tabs()}>
          {(tab) => (
            <Tabs.Content value={tab} class={styles.content}>
              <p>Content for {tab}.</p>
            </Tabs.Content>
          )}
        </For>
      </Tabs.Root>
    </div>
  )
}
.root {
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.list {
  display: flex;
  position: relative;
  z-index: 0;
  padding-inline: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset 0 -1px var(--color-gray-200);
}

.trigger {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.5rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.trigger[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .trigger:hover {
    color: var(--color-gray-900);
  }
}

.trigger[data-highlighted] {
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.trigger:focus-visible {
  position: relative;
}

.trigger:focus-visible::before {
  content: "";
  position: absolute;
  inset: 0.25rem 0;
  border-radius: 0.25rem;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

.indicator {
  position: absolute;
  z-index: -1;
  top: 50%;
  margin-top: -0.75rem;
  height: 1.5rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, width;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.content {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 8rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

.content:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.375rem;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

/* Vertical tabs layout */
.rootVertical {
  display: flex;
  width: 100%;
  max-width: 400px;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
}

.listVertical {
  display: flex;
  flex-direction: column;
  position: relative;
  z-index: 0;
  padding-block: 0.25rem;
  gap: 0.25rem;
  box-shadow: inset -1px 0 var(--color-gray-200);
}

.triggerVertical {
  display: flex;
  align-items: center;
  border: 0;
  margin: 0;
  outline: 0;
  background: none;
  appearance: none;
  color: var(--color-gray-600);
  font-family: inherit;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  user-select: none;
  white-space: nowrap;
  padding-inline: 0.75rem;
  padding-block: 0;
  height: 2rem;
  cursor: pointer;
}

.triggerVertical[data-selected] {
  color: var(--color-gray-900);
}

@media (hover: hover) {
  .triggerVertical:hover {
    color: var(--color-gray-900);
  }
}

.triggerVertical[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

.triggerVertical:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
  border-radius: 0.25rem;
}

.indicatorVertical {
  position: absolute;
  z-index: -1;
  left: 50%;
  margin-left: -2.5rem;
  width: 5rem;
  height: 2rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-100);
  transition-property: transform, height;
  transition-duration: 200ms;
  transition-timing-function: ease-in-out;
}

.contentVertical {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  outline: 0;
  font-size: 0.875rem;
  color: var(--color-gray-600);
  line-height: 1.5;
}

/* Dynamic tabs helpers */
.dynamicWrapper {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  max-width: 400px;
}

.dynamicControls {
  display: flex;
  gap: 0.5rem;
}

.dynamicButton {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  padding: 0 0.75rem;
  outline: 0;
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-gray-900);
  cursor: pointer;
}

.dynamicButton:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

@media (hover: hover) {
  .dynamicButton:hover:not(:disabled) {
    background-color: var(--color-gray-100);
  }
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}