Tabs

The tabs component is used to display a set of sections of content, each associated with a tab. Only one tab can be selected at a time.

WAI-ARIA Tabs Pattern

Features

Behavior

By default, when navigating between tabs with a keyboard, the focused tab will be opened. This can be changed by setting the option behavior to manual.

Keyboard navigation

  • ArrowLeft and ArrowRight keys can be used to navigate between tabs.
  • Home and End keys can be used to navigate to the first and last tab respectively.
  • Space and Enter keys can be used to select a tab.
  • Tab key can be used to navigate to the next focusable element.

Example

There are thousands of apple varieties, each with its own unique taste and texture. Apples come in a range of colors, including red, green, and yellow.

Sources

+page.svelte

<script lang="ts">
  import Tabs from './Tabs.svelte'
  import Tab from './Tab.svelte'
  import TabPanel from './TabPanel.svelte'
</script>

<Tabs>
  <svelte:fragment slot="tabs">
    <Tab key="apples" icon="🍎">Apples</Tab>
    <Tab key="oranges" icon="🍊">Oranges</Tab>
    <Tab key="grapes" icon="🍇">Grapes</Tab>
  </svelte:fragment>
  <TabPanel key="apples">
    There are thousands of apple varieties, each with its own unique taste and
    texture. Apples come in a range of colors, including red, green, and yellow.
  </TabPanel>
  <TabPanel key="oranges">
    They belong to the citrus family and have a refreshing, tangy flavor.
    Oranges come in various varieties, such as navel oranges and Valencia
    oranges.
  </TabPanel>
  <TabPanel key="grapes">
    They grow in clusters on vines and come in different colors, including
    green, red, and purple. Grapes are packed with antioxidants and are a good
    source of vitamin K.
  </TabPanel>
</Tabs>

Tab.svelte

<script lang="ts">
  import type { Tabs } from 'louisette'
  import { getContext } from 'svelte'

  export let key: string
  export let icon: string = ''

  const { tabAttrs, active } = getContext<Tabs>('tabs')
</script>

<div
  {...$tabAttrs(key)}
  class="flex cursor-pointer select-none flex-wrap items-center gap-1 rounded-md p-2 transition-colors hover:bg-neutral-100 focus:outline-none focus-visible:ring focus-visible:ring-accent-500 focus-visible:ring-opacity-50 dark:hover:bg-neutral-700 max-md:justify-center"
  class:is-active={$active === key}
>
  {#if icon}
    <span class="mr-1 text-sm" aria-hidden="true">{icon}</span>
  {/if}
  <slot />
</div>

<style lang="postcss">
  .is-active {
    @apply bg-neutral-300 dark:bg-neutral-700;
  }
</style>

TabPanel.svelte

<script lang="ts">
  import type { Tabs } from 'louisette'
  import { getContext } from 'svelte'

  export let key: string

  const { panelAttrs, active } = getContext<Tabs>('tabs')
</script>

{#if $active === key}
  <div {...$panelAttrs(key)}>
    <slot />
  </div>
{/if}

Tabs.svelte

<script lang="ts">
  import { createTabs } from 'louisette'
  import { setContext } from 'svelte'

  const { listAttrs, ...tabsContext } = createTabs()
  setContext('tabs', tabsContext)
</script>

<div
  class="flex flex-col gap-2 overflow-clip rounded-lg border border-neutral-200 bg-white p-4 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-100"
>
  <div {...$listAttrs} class="flex gap-2">
    <slot name="tabs" />
  </div>
  <slot />
</div>