Skip to content

Before you use this component

  • Runtime dependencies: bits-ui ^2.0.0.
  • Requires client-side JavaScript to work.
Palette Neutral

Neutral palette · 4.8 KB ZIP Receipt View code

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

<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

Svelte
<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 #

  1. Install the dependency: npm install bits-ui@^2.
  2. Copy the exported files into src/lib/components/feature-tabs-01/, keeping the structure:
Plain text
feature-tabs-01/
  FeatureTabs.svelte
  parts/FeaturePanel.svelte
  types.ts

Example #

Svelte
<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 extend FeatureTab in types.ts.
  • Active tab: edit the data-[state=active]: utilities on Tabs.Trigger.
  • Colours: replace the zinc utilities together; keep text contrast at 4.5:1 or better.
  • Activation: add activationMode="manual" to Tabs.Root to select tabs only on Enter/Space.

Limitations #

  • Requires bits-ui 2.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

NameTypeRequiredDefaultDescription
featuresFeatureTab[]YesNoneTabs in display order: { value, label, title, description, points? }. value must be unique.
titlestringYesNoneSection heading text; also labels the tab list.
descriptionstringNoNoneOptional introductory paragraph under the heading.
headingLevel2 | 3 | 4 | 5No2Level of the section heading; panel titles use the next level down.
valuestringNofeatures[0].valueSelected tab value. Bindable with bind:value.
onValueChange(value: string) => voidNoNoneCalled with the new value whenever the user selects a different tab. Local UI callback only.

Dependencies and services

PackageRangeResolved at build timePurpose
bits-ui^2.0.02.19.2Headless 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

Search components

Describe a section or control, such as “FAQ accordion” or “newsletter signup”.