Page Sections featurestabsinteractivearia
Feature Tabs Showcase
Interactive feature tabs following the WAI-ARIA tabs pattern with arrow-key navigation and stat panels.
Live Preview
One platform, every workflow
Switch between capabilities to see how teams use the kit.
Launch pages in an afternoon
Assemble hero, features, pricing and FAQ sections into conversion-ready landing pages.
- 24 new section blocks
- SEO-friendly markup
- Instant static builds
3.4× faster page launches
In-app analytics without a chart library
Render KPI tiles, funnels and cohort tables straight from your data at build time.
- 45 SVG charts
- Tooltips & table view
- Zero client JS
0 kB chart runtime shipped
Accessible components you can trust
Every component is keyboard navigable and screen-reader tested, with visible focus states.
- WCAG 2.2 AA
- Typed props
- Reduced-motion aware
100 Lighthouse accessibility
Usage
Copy and paste this code into your Astro page:
astro
---
import { FeatureTabsShowcase } from 'astro-component-kit/sections';
---
<FeatureTabsShowcase
variant="underline"
tabs={[
{ label: 'Marketing', title: 'Launch faster', text: '…', points: ['24 sections'], stat: '3.4×', statLabel: 'faster' },
{ label: 'Product', title: 'In-app analytics', text: '…', points: ['45 charts'], stat: '0 kB', statLabel: 'runtime' },
]}
/> ---
import { FeatureTabsShowcase } from 'astro-component-kit/sections';
---
<FeatureTabsShowcase
variant="underline"
tabs={[
{ label: 'Marketing', title: 'Launch faster', text: '…', points: ['24 sections'], stat: '3.4×', statLabel: 'faster' },
{ label: 'Product', title: 'In-app analytics', text: '…', points: ['45 charts'], stat: '0 kB', statLabel: 'runtime' },
]}
/> Manual Installation
If you are not using the npm package, create a new file and paste the following code: src/components/lib/FeatureTabsShowcase.astro:
astro
---
/**
* FeatureTabsShowcase — Interactive feature section using the WAI-ARIA tabs
* pattern (arrow keys, Home/End) to switch between product capabilities.
*
* @param {string} title - Section heading.
* @param {string} subtitle - Section description.
* @param {Array<{label: string, title: string, text: string, points: string[], stat: string, statLabel: string}>} tabs - Tab panels.
* @param {string} variant - Tab style (pills|underline|cards).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
interface Tab { label: string; title: string; text: string; points: string[]; stat: string; statLabel: string }
interface Props {
title?: string;
subtitle?: string;
tabs?: Tab[];
variant?: 'pills' | 'underline' | 'cards';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'One platform, every workflow',
subtitle = 'Switch between capabilities to see how teams use the kit.',
tabs = [
{ label: 'Marketing', title: 'Launch pages in an afternoon', text: 'Assemble hero, features, pricing and FAQ sections into conversion-ready landing pages.', points: ['24 new section blocks', 'SEO-friendly markup', 'Instant static builds'], stat: '3.4×', statLabel: 'faster page launches' },
{ label: 'Product', title: 'In-app analytics without a chart library', text: 'Render KPI tiles, funnels and cohort tables straight from your data at build time.', points: ['45 SVG charts', 'Tooltips & table view', 'Zero client JS'], stat: '0 kB', statLabel: 'chart runtime shipped' },
{ label: 'Engineering', title: 'Accessible components you can trust', text: 'Every component is keyboard navigable and screen-reader tested, with visible focus states.', points: ['WCAG 2.2 AA', 'Typed props', 'Reduced-motion aware'], stat: '100', statLabel: 'Lighthouse accessibility' },
],
variant = 'pills',
size = 'md',
className = '',
} = Astro.props;
const uid = Math.random().toString(36).slice(2, 8);
const id = `fts-${uid}`;
---
<div class="cq-feature-tabs-showcase-cq">
<section class={`fts fts--${variant} fts--${size} ${className}`} aria-labelledby={id}>
<div class="fts__inner">
<header class="fts__head">
<h2 id={id} class="fts__title">{title}</h2>
<p class="fts__subtitle">{subtitle}</p>
</header>
<div class="fts__tabs" data-fts>
<div class="fts__list" role="tablist" aria-label={title}>
{tabs.map((t, i) => (
<button
type="button"
role="tab"
class="fts__tab"
id={`${id}-tab-${i}`}
aria-controls={`${id}-panel-${i}`}
aria-selected={i === 0 ? 'true' : 'false'}
tabindex={i === 0 ? 0 : -1}
>{t.label}</button>
))}
</div>
{tabs.map((t, i) => (
<div class="fts__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} tabindex="0" hidden={i !== 0}>
<div class="fts__copy">
<h3 class="fts__panel-title">{t.title}</h3>
<p class="fts__text">{t.text}</p>
<ul class="fts__points">
{t.points.map((p) => (
<li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{p}</li>
))}
</ul>
</div>
<div class="fts__stat">
<span class="fts__stat-value">{t.stat}</span>
<span class="fts__stat-label">{t.statLabel}</span>
</div>
</div>
))}
</div>
</div>
</section>
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-fts]').forEach((root) => {
const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
const select = (next: HTMLButtonElement, focus = true) => {
tabs.forEach((t) => {
const on = t === next;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
const panel = document.getElementById(t.getAttribute('aria-controls') || '');
if (panel) panel.hidden = !on;
});
if (focus) next.focus();
};
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(tab, false));
tab.addEventListener('keydown', (e) => {
const map: Record<string, number> = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 };
if (!(e.key in map)) return;
e.preventDefault();
select(tabs[(map[e.key] + tabs.length) % tabs.length]);
});
});
});
</script>
<style>
.cq-feature-tabs-showcase-cq { container-type: inline-size; }
.fts {
--fts-py: 5.5rem;
padding: var(--fts-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.fts--sm { --fts-py: 3.5rem; }
.fts--lg { --fts-py: 7.5rem; }
.fts__inner { max-width: 1080px; margin: 0 auto; }
.fts__head { text-align: center; max-width: 640px; margin: 0 auto 2.5rem; }
.fts__title { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
.fts__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.fts__list { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; }
.fts__tab {
min-height: 44px;
padding: 0 1.25rem;
border: 1px solid transparent;
border-radius: var(--r-full, 999px);
background: transparent;
color: var(--c-text-2, #a3a3a3);
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.fts__tab:hover { color: var(--c-text-1, #fff); }
.fts__tab:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.fts--pills .fts__list { width: fit-content; margin-inline: auto; padding: 0.35rem; border-radius: var(--r-full, 999px); background: rgb(var(--ink-rgb, 255 255 255) / 0.04); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.07); }
.fts--pills .fts__tab[aria-selected='true'] { background: var(--c-text-1, #fff); color: var(--c-on-primary, #0a0a0a); }
.fts--underline .fts__list { border-bottom: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); gap: 1.5rem; }
.fts--underline .fts__tab { border-radius: 0; padding: 0 0.25rem; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.fts--underline .fts__tab[aria-selected='true'] { color: var(--c-text-1, #fff); border-bottom-color: var(--c-primary-light, #d4a017); }
.fts--cards .fts__tab { border-radius: var(--r-md, 12px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.08); }
.fts--cards .fts__tab[aria-selected='true'] { border-color: var(--c-primary-light, #d4a017); color: var(--c-text-1, #fff); background: var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 12%, transparent)); }
.fts__panel {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 2rem;
align-items: center;
padding: clamp(1.5rem, 4vw, 3rem);
border-radius: var(--r-xl, 24px);
background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.045), rgb(var(--ink-rgb, 255 255 255) / 0.01));
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07));
animation: fts-in 0.35s ease;
}
.fts__panel[hidden] { display: none; }
.fts__panel:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
@keyframes fts-in { from { opacity: 0; transform: translateY(6px); } }
.fts__panel-title { margin: 0 0 0.75rem; font-size: clamp(1.4rem, 3vw, 1.875rem); font-weight: 750; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
.fts__text { margin: 0 0 1.25rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.fts__points { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.fts__points li { display: flex; align-items: center; gap: 0.55rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.fts__points svg { color: var(--chart-pos, #34c38f); }
.fts__stat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 200px;
border-radius: var(--r-lg, 20px);
background: radial-gradient(circle at 50% 30%, var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 16%, transparent)), transparent 70%), rgb(var(--ink-rgb, 255 255 255) / 0.02);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06);
text-align: center;
padding: 1.5rem;
}
.fts__stat-value { font-size: clamp(2.75rem, 6vw, 4rem); font-weight: 800; letter-spacing: -0.04em; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; line-height: 1; }
.fts__stat-label { font-size: 0.875rem; color: var(--c-text-2, #a3a3a3); }
@container (max-width: 760px) { .fts__panel { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .fts__panel { animation: none; } }
</style> ---
/**
* FeatureTabsShowcase — Interactive feature section using the WAI-ARIA tabs
* pattern (arrow keys, Home/End) to switch between product capabilities.
*
* @param {string} title - Section heading.
* @param {string} subtitle - Section description.
* @param {Array<{label: string, title: string, text: string, points: string[], stat: string, statLabel: string}>} tabs - Tab panels.
* @param {string} variant - Tab style (pills|underline|cards).
* @param {string} size - Vertical rhythm (sm|md|lg).
* @param {string} className - Extra CSS class.
*/
interface Tab { label: string; title: string; text: string; points: string[]; stat: string; statLabel: string }
interface Props {
title?: string;
subtitle?: string;
tabs?: Tab[];
variant?: 'pills' | 'underline' | 'cards';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'One platform, every workflow',
subtitle = 'Switch between capabilities to see how teams use the kit.',
tabs = [
{ label: 'Marketing', title: 'Launch pages in an afternoon', text: 'Assemble hero, features, pricing and FAQ sections into conversion-ready landing pages.', points: ['24 new section blocks', 'SEO-friendly markup', 'Instant static builds'], stat: '3.4×', statLabel: 'faster page launches' },
{ label: 'Product', title: 'In-app analytics without a chart library', text: 'Render KPI tiles, funnels and cohort tables straight from your data at build time.', points: ['45 SVG charts', 'Tooltips & table view', 'Zero client JS'], stat: '0 kB', statLabel: 'chart runtime shipped' },
{ label: 'Engineering', title: 'Accessible components you can trust', text: 'Every component is keyboard navigable and screen-reader tested, with visible focus states.', points: ['WCAG 2.2 AA', 'Typed props', 'Reduced-motion aware'], stat: '100', statLabel: 'Lighthouse accessibility' },
],
variant = 'pills',
size = 'md',
className = '',
} = Astro.props;
const uid = Math.random().toString(36).slice(2, 8);
const id = `fts-${uid}`;
---
<div class="cq-feature-tabs-showcase-cq">
<section class={`fts fts--${variant} fts--${size} ${className}`} aria-labelledby={id}>
<div class="fts__inner">
<header class="fts__head">
<h2 id={id} class="fts__title">{title}</h2>
<p class="fts__subtitle">{subtitle}</p>
</header>
<div class="fts__tabs" data-fts>
<div class="fts__list" role="tablist" aria-label={title}>
{tabs.map((t, i) => (
<button
type="button"
role="tab"
class="fts__tab"
id={`${id}-tab-${i}`}
aria-controls={`${id}-panel-${i}`}
aria-selected={i === 0 ? 'true' : 'false'}
tabindex={i === 0 ? 0 : -1}
>{t.label}</button>
))}
</div>
{tabs.map((t, i) => (
<div class="fts__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} tabindex="0" hidden={i !== 0}>
<div class="fts__copy">
<h3 class="fts__panel-title">{t.title}</h3>
<p class="fts__text">{t.text}</p>
<ul class="fts__points">
{t.points.map((p) => (
<li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>{p}</li>
))}
</ul>
</div>
<div class="fts__stat">
<span class="fts__stat-value">{t.stat}</span>
<span class="fts__stat-label">{t.statLabel}</span>
</div>
</div>
))}
</div>
</div>
</section>
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-fts]').forEach((root) => {
const tabs = Array.from(root.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
const select = (next: HTMLButtonElement, focus = true) => {
tabs.forEach((t) => {
const on = t === next;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
const panel = document.getElementById(t.getAttribute('aria-controls') || '');
if (panel) panel.hidden = !on;
});
if (focus) next.focus();
};
tabs.forEach((tab, i) => {
tab.addEventListener('click', () => select(tab, false));
tab.addEventListener('keydown', (e) => {
const map: Record<string, number> = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 };
if (!(e.key in map)) return;
e.preventDefault();
select(tabs[(map[e.key] + tabs.length) % tabs.length]);
});
});
});
</script>
<style>
.cq-feature-tabs-showcase-cq { container-type: inline-size; }
.fts {
--fts-py: 5.5rem;
padding: var(--fts-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.fts--sm { --fts-py: 3.5rem; }
.fts--lg { --fts-py: 7.5rem; }
.fts__inner { max-width: 1080px; margin: 0 auto; }
.fts__head { text-align: center; max-width: 640px; margin: 0 auto 2.5rem; }
.fts__title { margin: 0 0 0.75rem; font-size: clamp(1.9rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -0.03em; color: var(--c-text-1, #fff); }
.fts__subtitle { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.fts__list { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; }
.fts__tab {
min-height: 44px;
padding: 0 1.25rem;
border: 1px solid transparent;
border-radius: var(--r-full, 999px);
background: transparent;
color: var(--c-text-2, #a3a3a3);
font: inherit;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.fts__tab:hover { color: var(--c-text-1, #fff); }
.fts__tab:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.fts--pills .fts__list { width: fit-content; margin-inline: auto; padding: 0.35rem; border-radius: var(--r-full, 999px); background: rgb(var(--ink-rgb, 255 255 255) / 0.04); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.07); }
.fts--pills .fts__tab[aria-selected='true'] { background: var(--c-text-1, #fff); color: var(--c-on-primary, #0a0a0a); }
.fts--underline .fts__list { border-bottom: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); gap: 1.5rem; }
.fts--underline .fts__tab { border-radius: 0; padding: 0 0.25rem; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.fts--underline .fts__tab[aria-selected='true'] { color: var(--c-text-1, #fff); border-bottom-color: var(--c-primary-light, #d4a017); }
.fts--cards .fts__tab { border-radius: var(--r-md, 12px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.08); }
.fts--cards .fts__tab[aria-selected='true'] { border-color: var(--c-primary-light, #d4a017); color: var(--c-text-1, #fff); background: var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 12%, transparent)); }
.fts__panel {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 2rem;
align-items: center;
padding: clamp(1.5rem, 4vw, 3rem);
border-radius: var(--r-xl, 24px);
background: linear-gradient(160deg, rgb(var(--ink-rgb, 255 255 255) / 0.045), rgb(var(--ink-rgb, 255 255 255) / 0.01));
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.07));
animation: fts-in 0.35s ease;
}
.fts__panel[hidden] { display: none; }
.fts__panel:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
@keyframes fts-in { from { opacity: 0; transform: translateY(6px); } }
.fts__panel-title { margin: 0 0 0.75rem; font-size: clamp(1.4rem, 3vw, 1.875rem); font-weight: 750; letter-spacing: -0.02em; color: var(--c-text-1, #fff); }
.fts__text { margin: 0 0 1.25rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.fts__points { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.fts__points li { display: flex; align-items: center; gap: 0.55rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.fts__points svg { color: var(--chart-pos, #34c38f); }
.fts__stat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 200px;
border-radius: var(--r-lg, 20px);
background: radial-gradient(circle at 50% 30%, var(--c-primary-glow, color-mix(in srgb, var(--c-primary, #b8860b) 16%, transparent)), transparent 70%), rgb(var(--ink-rgb, 255 255 255) / 0.02);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06);
text-align: center;
padding: 1.5rem;
}
.fts__stat-value { font-size: clamp(2.75rem, 6vw, 4rem); font-weight: 800; letter-spacing: -0.04em; color: var(--c-text-1, #fff); font-variant-numeric: tabular-nums; line-height: 1; }
.fts__stat-label { font-size: 0.875rem; color: var(--c-text-2, #a3a3a3); }
@container (max-width: 760px) { .fts__panel { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .fts__panel { animation: none; } }
</style>
Quick Info
- Category
- Page Sections
- Filename
FeatureTabsShowcase.astro- Dependencies
- None — pure Astro + CSS
- Tags
- featurestabsinteractivearia