Feature Tabs Showcase
Interactive feature tabs following the WAI-ARIA tabs pattern with arrow-key navigation and stat panels.
featurestabsinteractivearia
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 kBchart 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
100Lighthouse accessibility
Usage
Copy and paste this code into your React component:
tsx
import { FeatureTabsShowcase } from 'react-component-kit-ui/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/sections/FeatureTabsShowcase.tsx + FeatureTabsShowcase.css:
FeatureTabsShowcase.tsx
'use client';
import { useEffect, useRef } from 'react';
import './FeatureTabsShowcase.css';
interface Tab { label: string; title: string; text: string; points: string[]; stat: string; statLabel: string }
/**
* 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.
*/
export interface FeatureTabsShowcaseProps {
title?: string;
subtitle?: string;
tabs?: Tab[];
variant?: 'pills' | 'underline' | 'cards';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export function FeatureTabsShowcase({
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 = '',
}: FeatureTabsShowcaseProps) {
const uid = Math.random().toString(36).slice(2, 8);
const id = `fts-${uid}`;
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
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]);
});
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className="cq-feature-tabs-showcase-cq" data-rk-6565e6="">
<section className={`fts fts--${variant} fts--${size} ${className}`} aria-labelledby={id} data-rk-6565e6="">
<div className="fts__inner" data-rk-6565e6="">
<header className="fts__head" data-rk-6565e6="">
<h2 id={id} className="fts__title" data-rk-6565e6="">{title}</h2>
<p className="fts__subtitle" data-rk-6565e6="">{subtitle}</p>
</header>
<div className="fts__tabs" data-fts data-rk-6565e6="">
<div className="fts__list" role="tablist" aria-label={title} data-rk-6565e6="">
{tabs.map((t, i) => (
<button key={i} type="button" role="tab" className="fts__tab" id={`${id}-tab-${i}`} aria-controls={`${id}-panel-${i}`} aria-selected={i === 0 ? 'true' : 'false'} tabIndex={i === 0 ? 0 : -1} data-rk-6565e6="">{t.label}</button>
))}
</div>
{tabs.map((t, i) => (
<div key={i} className="fts__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} tabIndex={0} hidden={i !== 0} data-rk-6565e6="">
<div className="fts__copy" data-rk-6565e6="">
<h3 className="fts__panel-title" data-rk-6565e6="">{t.title}</h3>
<p className="fts__text" data-rk-6565e6="">{t.text}</p>
<ul className="fts__points" data-rk-6565e6="">
{t.points.map((p, __i: number) => (
<li key={__i} data-rk-6565e6=""><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-6565e6=""><path d="M20 6 9 17l-5-5" data-rk-6565e6="" /></svg>{p}</li>
))}
</ul>
</div>
<div className="fts__stat" data-rk-6565e6="">
<span className="fts__stat-value" data-rk-6565e6="">{t.stat}</span>
<span className="fts__stat-label" data-rk-6565e6="">{t.statLabel}</span>
</div>
</div>
))}
</div>
</div>
</section>
</div>
</>
);
}
export default FeatureTabsShowcase;FeatureTabsShowcase.css
/* FeatureTabsShowcase โ ported from astro-component-kit (sections/FeatureTabsShowcase.astro) */
.cq-feature-tabs-showcase-cq:where([data-rk-6565e6]) { container-type: inline-size; }
.fts:where([data-rk-6565e6]) {
--fts-py: 5.5rem;
padding: var(--fts-py) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
}
.fts--sm:where([data-rk-6565e6]) { --fts-py: 3.5rem; }
.fts--lg:where([data-rk-6565e6]) { --fts-py: 7.5rem; }
.fts__inner:where([data-rk-6565e6]) { max-width: 1080px; margin: 0 auto; }
.fts__head:where([data-rk-6565e6]) { text-align: center; max-width: 640px; margin: 0 auto 2.5rem; }
.fts__title:where([data-rk-6565e6]) { 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:where([data-rk-6565e6]) { margin: 0; font-size: 1.0625rem; color: var(--c-text-2, #a3a3a3); }
.fts__list:where([data-rk-6565e6]) { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; }
.fts__tab:where([data-rk-6565e6]) {
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:where([data-rk-6565e6]) { color: var(--c-text-1, #fff); }
.fts__tab:focus-visible:where([data-rk-6565e6]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.fts--pills:where([data-rk-6565e6]) .fts__list:where([data-rk-6565e6]) { 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:where([data-rk-6565e6]) .fts__tab[aria-selected='true']:where([data-rk-6565e6]) { background: var(--c-text-1, #fff); color: var(--c-on-primary, #0a0a0a); }
.fts--underline:where([data-rk-6565e6]) .fts__list:where([data-rk-6565e6]) { border-bottom: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08); gap: 1.5rem; }
.fts--underline:where([data-rk-6565e6]) .fts__tab:where([data-rk-6565e6]) { border-radius: 0; padding: 0 0.25rem; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.fts--underline:where([data-rk-6565e6]) .fts__tab[aria-selected='true']:where([data-rk-6565e6]) { color: var(--c-text-1, #fff); border-bottom-color: var(--c-primary-light, #d4a017); }
.fts--cards:where([data-rk-6565e6]) .fts__tab:where([data-rk-6565e6]) { border-radius: var(--r-md, 12px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.08); }
.fts--cards:where([data-rk-6565e6]) .fts__tab[aria-selected='true']:where([data-rk-6565e6]) { 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:where([data-rk-6565e6]) {
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: feature-tabs-showcase-fts-in 0.35s ease;
}
.fts__panel[hidden]:where([data-rk-6565e6]) { display: none; }
.fts__panel:focus-visible:where([data-rk-6565e6]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
@keyframes feature-tabs-showcase-fts-in { from { opacity: 0; transform: translateY(6px); } }
.fts__panel-title:where([data-rk-6565e6]) { 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:where([data-rk-6565e6]) { margin: 0 0 1.25rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
.fts__points:where([data-rk-6565e6]) { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.fts__points:where([data-rk-6565e6]) li:where([data-rk-6565e6]) { display: flex; align-items: center; gap: 0.55rem; color: var(--c-text-1, #fff); font-size: 0.9375rem; }
.fts__points:where([data-rk-6565e6]) svg:where([data-rk-6565e6]) { color: var(--chart-pos, #34c38f); }
.fts__stat:where([data-rk-6565e6]) {
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:where([data-rk-6565e6]) { 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:where([data-rk-6565e6]) { font-size: 0.875rem; color: var(--c-text-2, #a3a3a3); }
@container (max-width: 760px) { .fts__panel:where([data-rk-6565e6]) { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .fts__panel:where([data-rk-6565e6]) { animation: none; } }