Why CSS-Only Tabs Are Faster Than React Tabs
A deep dive into the radio-button pattern that powers this template's tab component — zero JavaScript, full ARIA support.
Every component in the starter, demonstrated live with code. Modern CSS features replacing JavaScript libraries — zero-JS by default, full accessibility.
Single source of truth in tokens/. Built to CSS custom properties, consumed via Tailwind utilities. Every color on this page comes from the token system.
--color-primary-500 --color-primary-600 --color-primary-700 --color-secondary-500 --color-secondary-600 --color-secondary-700 --color-background-primary --color-background-secondary --color-foreground-primary Primitive building blocks. Each is a single Astro component with TypeScript props, design token colors, and built-in accessibility.
Inline label element with variant and size options. Enhanced with prefers-contrast support for users who request higher contrast.
prefers-contrast: more — heavier borders and font weight
<Badge variant="primary" size="sm">Primary</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="neutral">Neutral</Badge> Polymorphic button/link component. Renders as <a> when href is provided, <button> otherwise. Supports disabled state with proper aria.
Disabled state uses aria-disabled and removes from tab order
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost" size="sm">Ghost</Button> Hover/focus tooltip upgraded with the Popover API. Browsers that support popover='hint' get auto-dismiss and Escape-to-close. Others fall back to CSS hover/focus.
Visible on focus + hover; Escape-to-close (Popover API)
<Tooltip text="Helpful information" position="top">
Hover me
</Tooltip> Scroll-driven reveal animations. Replaces AOS, ScrollMagic, and GSAP ScrollTrigger with zero JavaScript. Uses animation-timeline: view() — content animates in as it enters the viewport.
prefers-reduced-motion: respected — content visible immediately
Scroll down to see these cards reveal (they use the same ScrollReveal wrapping this entire page):
<ScrollReveal animation="fade-up">
<Card>Content reveals on scroll</Card>
</ScrollReveal> Animated counter using CSS @property with <integer> type and counter(). Numbers count up from 0 when scrolled into view — entirely in CSS.
role=img + aria-label exposes counter values to assistive tech
<CounterBadge value={42} suffix="+" label="Components" />
Composite components built from atoms. Interactive patterns using native HTML elements — <dialog>, <details>, radio buttons — instead of JavaScript frameworks.
Base container with optional @starting-style entry animation. When animated=true, the card fades and slides in on first render — visible during View Transitions navigation.
prefers-reduced-motion: respected — no entry animation
<Card animated>
<div class="p-4">Content with entry animation</div>
</Card> Native <dialog> element with ::backdrop, focus trapping, and Escape-to-close — all browser-provided. Replaces Radix Dialog, HeadlessUI, or any modal library. Entry animation via @starting-style.
Focus trap + Escape-to-close: native to <dialog>
<Dialog id="my-dialog" title="Dialog Title" size="md">
<p>Dialog content here</p>
</Dialog>
<button onclick="document.getElementById('my-dialog').showModal()">
Open Dialog
</button> CSS-only tab switching using hidden radio buttons — the same pattern as the Header's mobile menu checkbox. Arrow key navigation via ~15 lines of inline script (WCAG requirement).
Keyboard: arrow keys, Home, End — full WCAG tablist pattern
Panel switching is powered by CSS :has() selectors and the peer-checked: pattern. No framework state management needed.
Pass an array of tab objects. Each panel is a child with data-tab-panel matching the tab ID. The defaultTab prop sets the initially active tab.
Full ARIA support: role="tablist", role="tab", role="tabpanel", aria-selected, aria-controls. Arrow keys navigate between tabs.
<Tabs tabs={[{ id: "one", label: "Tab One" }, ...]}>
<div data-tab-panel="one" role="tabpanel">First panel</div>
<div data-tab-panel="two" role="tabpanel">Second panel</div>
</Tabs> Layout components that form the page skeleton. The Grid component uses CSS Container Queries — responding to parent width instead of viewport width.
Responsive grid that uses @container queries instead of media queries. The grid responds to its parent container's width, not the viewport — making it truly reusable in any layout context.
<Grid cols={3} gap={4}>
<Card>Item 1</Card>
<Card>Item 2</Card>
<Card>Item 3</Card>
</Grid> CSS scroll-driven parallax. Decorative background shapes move at different rates than content using animation-timeline: scroll(). Replaces Rellax, Locomotive Scroll, or GSAP ScrollTrigger.
prefers-reduced-motion: respected — shapes stay static; aria-hidden on decorations
The hero section at the top of this page uses ParallaxSection. Scroll up to see the subtle background shape movement as you scroll past.
<ParallaxSection intensity="subtle">
<h2>Content moves at normal speed</h2>
<p>Background shapes drift with parallax</p>
</ParallaxSection> CSS handles presentation. JavaScript handles state — only when you genuinely need it. This is the one island component in the showcase: Preact Signals for fine-grained reactivity with zero VDOM diffing.
Preact Signals reactive state demo. Uses client:visible for lazy hydration — zero JS cost until the component enters the viewport. Signal updates only re-render the text nodes that changed, not the entire component tree.
aria-label on increment/decrement buttons + visible focus rings
Reactive Count
0
Preact Signals — fine-grained reactivity with zero VDOM diffing. Only the text nodes that change re-render.
import SignalsCounter from "@/components/islands/SignalsCounter.tsx";
<SignalsCounter client:visible /> Isolated demos prove a component renders. Compositions prove the system holds together. Below: real UI assembled from the same atoms and molecules shown above — no glue code.
Card + Badge + Tooltip + Button — the building blocks for any blog index, news feed, or content listing. Hover the date to see the Tooltip, click the button for the Card hover state.
All interactive elements keyboard-reachable; tooltip on focus + hover
A deep dive into the radio-button pattern that powers this template's tab component — zero JavaScript, full ARIA support.
<Card animated>
<article class="p-6">
<Badge variant="primary" size="xs">Engineering</Badge>
<h3>Why CSS-Only Tabs Are Faster Than React Tabs</h3>
<p>A deep dive into the radio-button pattern...</p>
<Tooltip text="Published 4 days ago">
<time>Apr 7, 2026</time>
</Tooltip>
<Button variant="ghost" size="sm" href="/blog/css-tabs/">Read more</Button>
</article>
</Card> Tabs + Dialog + Grid + Button — a typical app settings UI. The tabs partition concerns, the dialog handles destructive confirmations, the grid lays out option cards.
Tabs: arrow keys + Home/End. Dialog: focus trap + Escape. Both built into the components.
Follows system preference
English (US)
All enabled
Advanced settings would live here — feature flags, experimental UI, developer tools. Try the arrow keys to navigate between tabs.
<Tabs tabs={[
{ id: "general", label: "General" },
{ id: "advanced", label: "Advanced" },
]}>
<div data-tab-panel="general">
<Grid>
<Card>Theme</Card>
<Card>Language</Card>
</Grid>
<Button onclick="confirmDialog.showModal()">Reset all</Button>
</div>
</Tabs>
<Dialog id="confirm-reset" title="Reset all settings?">
<p>This cannot be undone.</p>
</Dialog>