Feature tabs
A feature showcase section that switches between product capabilities using accessible tabs built on Bits UI.
cmp_feature_tabs_001 Before you use this component
- Runtime dependencies: bits-ui ^2.0.0.
- Requires client-side JavaScript to work.
Preview
Fit to the available width. Previews taller than the maximum auto-height scroll inside the frame.
Use this component with your coding agent
Using the PageSugar MCP server, fetch component cmp_feature_tabs_001 version 1.0.0 with variant "neutral", then add its files to this project and follow its usage notes.
Code
Neutral palette, version 1.0.0. Artifact sha256-18ec61829023b8334d3d9be23df9b97b95562d770213afb31feb7d521d5e6280
Files (3)
This component is distributed as the complete file tree below. Copying one file is not enough; download the bundle to get every file.
- FeatureTabs.svelte Svelte component · 2.6 KB· entry
- FeaturePanel.svelte Svelte component · 1.2 KB
- types.ts TypeScript · 449 B
FeatureTabs.svelte · Svelte component · 2.6 KB
<script lang="ts">
import { Tabs } from 'bits-ui';
import FeaturePanel from './parts/FeaturePanel.svelte';
import type { FeatureTab, HeadingLevel } from './types';
interface Props {
features: FeatureTab[];
title: string;
description?: string;
headingLevel?: HeadingLevel;
value?: string;
onValueChange?: (value: string) => void;
}
let {
features,
title,
description,
headingLevel = 2,
value = $bindable(features[0]?.value ?? ''),
onValueChange
}: Props = $props();
const uid = $props.id();
const sectionHeading = $derived(`h${headingLevel}`);
const panelHeadingLevel = $derived((headingLevel + 1) as 3 | 4 | 5 | 6);
</script>
<section class="px-4 py-16 sm:px-6 lg:px-8" aria-labelledby="{uid}-title">
<div class="mx-auto max-w-5xl">
<div class="max-w-2xl">
<svelte:element
this={sectionHeading}
id="{uid}-title"
class="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl"
>
{title}
</svelte:element>
{#if description}
<p class="mt-4 text-base text-pretty text-zinc-600 sm:text-lg">{description}</p>
{/if}
</div>
<Tabs.Root bind:value {onValueChange} class="mt-10">
<Tabs.List
aria-labelledby="{uid}-title"
class="-mx-4 flex gap-6 overflow-x-auto border-b border-zinc-200 px-4 sm:mx-0 sm:px-0"
>
{#each features as feature, index (feature.value)}
<Tabs.Trigger
value={feature.value}
id="{uid}-tab-{index}"
class="-mb-px shrink-0 border-b-2 border-transparent py-3 text-sm font-medium whitespace-nowrap text-zinc-600 hover:text-zinc-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-zinc-900 disabled:opacity-50 data-[state=active]:border-zinc-900 data-[state=active]:text-zinc-900"
>
{#snippet child({ props })}
<!-- Explicit relationships and roving tabindex so server-rendered markup is complete before hydration. -->
<button
{...props}
aria-controls="{uid}-panel-{index}"
tabindex={!value || value === feature.value ? 0 : -1}
>
{feature.label}
</button>
{/snippet}
</Tabs.Trigger>
{/each}
</Tabs.List>
{#each features as feature, index (feature.value)}
<Tabs.Content
value={feature.value}
id="{uid}-panel-{index}"
class="pt-8 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-zinc-900"
>
{#snippet child({ props })}
<div {...props} aria-labelledby="{uid}-tab-{index}">
<FeaturePanel {feature} headingLevel={panelHeadingLevel} />
</div>
{/snippet}
</Tabs.Content>
{/each}
</Tabs.Root>
</div>
</section>
Usage
Supply feature tabs as data. The selected tab is local state; bind value or pass onValueChange to observe or control it. Requires the bits-ui package; no backend.
Suggested location: src/lib/components/feature-tabs-01
Required props: features, title
Example
<script lang="ts">
import FeatureTabs from '$lib/components/feature-tabs-01/FeatureTabs.svelte';
import type { FeatureTab } from '$lib/components/feature-tabs-01/types';
const features: FeatureTab[] = [
{ value: 'plan', label: 'Plan', title: 'Plan work in one place', description: 'Collect tasks, owners and dates on a shared board.', points: ['Shared boards', 'Due dates'] },
{ value: 'track', label: 'Track', title: 'See progress at a glance', description: 'Status updates roll up into a weekly summary.' }
];
let selected = $state('plan');
</script>
<FeatureTabs title="How it works" {features} bind:value={selected} />Limitations
- Install bits-ui (npm install bits-ui@^2) before using the component.
- Panel content is plain text; edit FeaturePanel.svelte to add media or rich content.
- Every feature value must be unique; it is used as the tab key.
- Tab switching needs JavaScript; before hydration only the initially selected panel is visible.
Feature tabs #
A feature showcase that switches between capabilities with accessible tabs. The selected tab is local
UI state. It uses the headless Tabs primitive from Bits UI; there is no backend.
Install #
- Install the dependency:
npm install bits-ui@^2. - Copy the exported files into
src/lib/components/feature-tabs-01/, keeping the structure:
feature-tabs-01/
FeatureTabs.svelte
parts/FeaturePanel.svelte
types.tsExample #
<script lang="ts">
import FeatureTabs from '$lib/components/feature-tabs-01/FeatureTabs.svelte';
import type { FeatureTab } from '$lib/components/feature-tabs-01/types';
const features: FeatureTab[] = [
{
value: 'plan',
label: 'Plan',
title: 'Plan work in one place',
description: 'Collect tasks, owners and dates on a shared board.',
points: ['Shared boards', 'Due dates']
},
{
value: 'track',
label: 'Track',
title: 'See progress at a glance',
description: 'Status updates roll up into a weekly summary.'
}
];
let selected = $state('plan');
</script>
<FeatureTabs title="How it works" {features} bind:value={selected} />Props #
| Prop | Type | Default | Notes |
|---|---|---|---|
features |
FeatureTab[] |
required | value must be unique. |
title |
string |
required | Section heading; labels the tab list. |
description |
string |
— | Intro paragraph. |
headingLevel |
2 | 3 | 4 | 5 |
2 |
Panel titles use headingLevel + 1. |
value |
string |
features[0].value |
Bindable selected tab. |
onValueChange |
(value: string) => void |
— | Fires when the user selects another tab. |
Customization #
- Panel content: edit
parts/FeaturePanel.svelte(for example, add an image) and extendFeatureTabintypes.ts. - Active tab: edit the
data-[state=active]:utilities onTabs.Trigger. - Colours: replace the
zincutilities together; keep text contrast at 4.5:1 or better. - Activation: add
activationMode="manual"toTabs.Rootto select tabs only on Enter/Space.
Limitations #
- Requires
bits-ui2.x. - Panels are plain text by default.
- Switching tabs needs JavaScript; before hydration only the initial panel is shown.
- Light appearance only.
Customization
Change tabs through the features prop, edit the panel layout in parts/FeaturePanel.svelte, and edit Tailwind classes for colours and spacing. No colour tokens are declared.
- Content: add, remove or reorder entries in features; keep each value unique and stable.
- Panel layout: edit parts/FeaturePanel.svelte to add an image, link or different grid; extend FeatureTab in types.ts to match.
- Active tab style: change the data-[state=active]: border and text utilities on Tabs.Trigger.
- Colours: replace the zinc text, border and outline utilities together and keep text at 4.5:1 contrast.
- Activation: Bits UI activates tabs on focus by default; add activationMode="manual" to Tabs.Root if panels are expensive to render.
- Headings: set headingLevel to fit the page outline; panel titles use the next level.
Props and content inputs
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
features | FeatureTab[] | Yes | None | Tabs in display order: { value, label, title, description, points? }. value must be unique. |
title | string | Yes | None | Section heading text; also labels the tab list. |
description | string | No | None | Optional introductory paragraph under the heading. |
headingLevel | 2 | 3 | 4 | 5 | No | 2 | Level of the section heading; panel titles use the next level down. |
value | string | No | features[0].value | Selected tab value. Bindable with bind:value. |
onValueChange | (value: string) => void | No | None | Called with the new value whenever the user selects a different tab. Local UI callback only. |
Dependencies and services
| Package | Range | Resolved at build time | Purpose |
|---|---|---|---|
bits-ui | ^2.0.0 | 2.19.2 | Headless Tabs primitive providing tab/tablist/tabpanel roles, roving focus and arrow-key navigation. |
Install with (shown for reference, run it yourself):
npm install bits-ui@^2.0.0 Integration boundaries
- Integration level: local-interaction.
- Requires client-side JavaScript to be interactive.
- Server-side rendering: supported.
Accessibility
- Implements the WAI-ARIA tabs pattern through Bits UI: tablist, tab and tabpanel roles with aria-selected, aria-controls and aria-labelledby.
- Arrow keys move between tabs (looping), Home and End jump to the first and last tab, and only the selected tab is in Tab order.
- The tab list is labelled by the section heading; the selected tab is indicated by text colour and an underline, not colour alone.
- Panels are focusable so keyboard users can reach panel content directly after the tab list; focus is shown with a visible outline.
- Tab and panel IDs, aria-controls, aria-labelledby and the roving tabindex are set explicitly from $props.id() and the selected value, so server-rendered markup is complete before hydration and multiple instances never collide.
Known limitations
- On narrow screens tabs scroll horizontally; there is no visible scroll affordance beyond the clipped last tab.
License
Source license: MIT.
Version history
Only one release has been published so far; earlier versions will be listed here.
- 1.0.0 Published current · selected · 16 September 2026