
Pure CSS Vertical Tabs (No JavaScript)
Published
18 hand-coded CSS vertical tabs for SaaS settings panels, documentation sidebars, product feature marketing pages, and pricing comparison layouts — pure CSS with zero JavaScript, responsive vertical-to-mobile-accordion, SVG icons + text labels, icon-only mini sidebar with tooltips (VS Code / Discord / Slack pattern), sliding active-indicator bar, Bootstrap 5 + Tailwind templates, native <details>/<summary> hybrid, SaaS admin settings shell, docs sidebar with category groups, :has() active state, View Transitions API, dark mode, glassmorphism, and a WAI-ARIA accessible reference (WCAG 2.2 AA). For the general tabs mechanic hub (horizontal tabs, 32 patterns) see CSS Tabs. 12 Pure CSS + 6 Light JS. Scoped under .vt-NN for no-collision pasting, prefers-reduced-motion guarded per WCAG 2.3.3, framework-agnostic (React, Vue, Svelte, Astro, Next, Nuxt).
Related32 CSS Tab Designs32 CSS Accordions — Vertical & Horizontal30 CSS Sidebar Layouts

Published

Published
Published
Published

:has() lookup maps the checked radio to an index integer, and the marker rides translateY(calc(var(--i) * var(--step))) on a spring curve. One rule per tab, no ResizeObserver, no getBoundingClientRect.Published

.d-flex + .nav.flex-column.nav-pills + .tab-content — with the exact class names, data attributes and ARIA wiring the framework expects, so it drops straight into a Bootstrap project. Ships a 24-line stand-in script and a scoped stylesheet so it also runs with no Bootstrap on the page at all.Published

aria-selected: variant: state lives in one ARIA attribute, styling reacts to it declaratively, and the script never touches a class list.Published

<details> disclosures — then let CSS promote it to a two-column vertical tab set on wide screens. The most robust pattern in this collection: no JS, no ARIA to get wrong, and full keyboard and screen-reader support straight from the HTML spec.Published

Published

Published

:has() decides which one is visible, so the annual saving is honest, the markup stays flat, and there is no JavaScript to hydrate.Published

Published

:has().Published

view-transition-name declarations. The browser does the work it used to take FLIP libraries to fake.Published

<details> elements the same name and the browser enforces one-open-at-a-time, exactly like radio buttons. Paired with ::details-content, interpolate-size and @starting-style, you get a fully animated exclusive tab stack in pure HTML and CSS.Published

data-theme attribute — every colour in the component is derived from that, so adding a third theme is a single CSS block, not a refactor.Published

backdrop-filter stack with saturation boost, a hairline light border, an inner highlight along the top edge, and — the part most tutorials skip — a contrast floor so the text stays legible no matter what photo scrolls behind it.Published

Published
name='vt-tabs'. Position them opacity: 0; position: absolute; pointer-events: none so they stay in the accessibility tree (keyboard-toggleable) but invisible. (2) Tab labels: <label for='tab-N'> elements styled as the visible tab buttons. Clicking a label flips the associated radio — zero JavaScript. (3) Panels: N content panels, one per tab, each hidden by default (display: none). (4) The sibling-selector magic: input#tab-1:checked ~ .panels .panel-1 { display: block }. When a radio is checked, its sibling panels container's matching panel becomes visible. Every browser back to IE9 supports this. The trap: this pattern requires the radio inputs and panels to share a common ancestor with the sibling selector reaching from radios to panels. The DOM structure MUST be: <div> [radios] [labels] <div class='panels'>[panels]</div> </div>. If you nest the panels inside a wrapper the sibling selector can't reach into, the pattern breaks. Accessibility: the radios stay in the tab order, keyboard users Tab to them and use arrow keys to move between tabs (native radio-group arrow-key nav). Screen readers announce "radio button, 1 of N, checked/not checked". Not ideal — the WAI-ARIA tab pattern would announce "tab, 1 of N, selected". Trade-off: pure CSS = radio semantics; WAI-ARIA tab semantics = requires JS. Demo 18 (WAI-ARIA reference) covers the JS-enhanced version. Why ship pure CSS at all: zero JavaScript = zero INP tax on Core Web Vitals. Zero runtime = zero bundle. Zero framework = works in any HTML page instantly. Perfect for landing pages, static sites, and progressive-enhancement patterns where JS is unavailable. Compare to shipping @radix-ui/react-tabs (~8KB Radix + React dep) or @headlessui/react Tab (~35KB HeadlessUI + React dep) or @mui/material Tabs (~85KB MUI + Emotion runtime) — the pure CSS in Demo 01 is 2-4KB, INP-safe (compositor-only), CLS 0, works with JavaScript disabled.<details>/<summary> elements. On mobile they naturally render as accordion. On desktop, CSS overrides them into a vertical tab layout via grid-template-columns. (2) Container-query breakpoint (not viewport): use @container (min-width: 900px) on the tabs wrapper. When the tabs container itself has ≥ 900px available, switch to vertical layout. Container queries beat viewport queries here because the tabs might live inside a narrow column (article sidebar, docs page center column) where viewport width doesn't reflect the actual available space. (3) Desktop layout override: @container (min-width: 900px) { .tabs { display: grid; grid-template-columns: 240px 1fr; } .tabs details { display: contents; } .tabs summary { grid-column: 1; } .tabs details[open] > *:not(summary) { grid-column: 2; grid-row: 1 / -1; }} — the display: contents on details lets grid position summary + content INDEPENDENTLY. On mobile the details/summary work natively. Tier-1 SaaS canonical implementations: Stripe Dashboard settings, Linear Settings, Vercel Dashboard settings, Notion workspace settings, Framer project settings, Superhuman preferences, Airbnb host settings, Shopify admin settings. All use the vertical-on-desktop / accordion-on-mobile pattern because it optimizes for both device classes without maintaining two separate components. Tier-1 CPM: SaaS admin dashboards $8-25 CPM; enterprise SaaS $15-25 with Notion Enterprise / Airtable Business / Coda Team premium tier. Every SaaS RFP with an admin-portal screenshot has this pattern in it.<div role='tablist' aria-orientation='vertical'> wraps the tabs. Each tab is <button role='tab' aria-selected='true|false' aria-controls='panel-N' id='tab-N' tabindex='0|-1'>. Each panel is <div role='tabpanel' aria-labelledby='tab-N' tabindex='0'>. (2) aria-orientation='vertical' tells assistive tech to expect vertical layout — screen readers change their navigation announcements accordingly. (3) Roving tabindex: only ONE tab has tabindex='0' at a time (the currently-focused/selected one). All others have tabindex='-1'. This means Tab moves focus INTO and OUT OF the tablist as a single unit (not through every tab). Arrow keys move focus WITHIN the tablist. (4) ArrowDown / ArrowUp: move focus to the next / previous tab. On vertical orientation, ArrowDown/Up are the primary navigation keys (on horizontal orientation the primary keys are ArrowLeft/Right). (5) Home / End: focus first / last tab. Non-negotiable per WAI-ARIA APG. (6) Space / Enter: activate the currently-focused tab (change aria-selected + show the associated panel). (7) Focus visible per WCAG 2.4.13: :focus-visible outline at 2px min / 3:1 contrast on the tab. Never outline: none without visible replacement. (8) Automatic vs manual activation: WAI-ARIA APG defines two variants — automatic activation (arrow keys immediately change the active panel; use for lightweight panel content) vs manual activation (arrow keys only move focus; Space/Enter activates; use for heavy panel content that would jank on rapid arrow-key sweeps). Demo 18 defaults to manual activation because SaaS settings panels often contain forms that would reset if the panel changed on every arrow-key press. Legal enforcement in tier-1 markets: ADA Title III lawsuits over inaccessible websites hit ~4000/year since 2019 (Domino's Pizza v. Robles precedent at $50K per site). Section 508 mandatory since 1998 for federal contracts. EU EAA / Canada ACA / Australia DDA / UK Equality Act 2010 interpret 'reasonable accommodation' as WCAG 2.1/2.2 AA. Enterprise procurement RFPs (Salesforce, ServiceNow, Workday, Oracle, SAP) list WCAG 2.2 AA as a hard requirement — a non-compliant tab pattern kills a $500K contract. Tier-1 CPM: enterprise SaaS $25-45, government $30-60 (Section 508 mandatory), healthcare $30-50 (HIPAA-adjacent + Section 508 for federal-funded providers).<details> elements with a shared name attribute, and the browser enforces mutual exclusion natively. When one opens, the previously-open one automatically closes. Zero JavaScript. Zero click handlers. Zero state management. The recipe: <details name='vt-tabs' open><summary>Tab 1</summary><p>Content 1</p></details> <details name='vt-tabs'><summary>Tab 2</summary><p>Content 2</p></details> — every <details> with the same name is in the same exclusive group. To render as vertical tabs (not stacked accordion), wrap them in display: grid; grid-template-columns: 240px 1fr with the same display: contents trick from Demo 02. Why this beats radio-button hack: (a) real disclosure semantics for screen readers (they announce "disclosure triangle, expanded" vs the radio hack's "radio button, checked"), (b) native browser Find-in-Page auto-opens the correct panel when text is found inside, (c) native browser back/forward navigation preserves which panel is open, (d) works without ANY JavaScript, no hidden inputs required. The Chromium quirk to know: before Baseline (pre-September 2024), some Chromium versions ignored the name attribute silently — items opened independently, no error. Progressive enhancement: if the browser ignores name, users get a multi-open accordion instead of exclusive-vertical-tabs — a graceful degradation, not a broken UI. In 2026 traffic on tier-1 US/UK/CA/AU/NZ markets, > 97% of visitors get the native exclusive behavior; the remaining 3% get a working multi-open accordion. Ship it. Framework portability: works in every framework as-is because it's native HTML. React, Vue, Svelte, Astro, Next, Nuxt, Remix — all accept the markup with zero changes. Compare to shipping @radix-ui/react-tabs (~8KB Radix + React) or @headlessui/react Tab (~35KB HeadlessUI + React) or @mui/material Tabs (~85KB MUI + Emotion) — Demo 15 is 1KB of pure HTML + CSS with better semantics.class to className, drop the JSX into any server or client component. The 12 Pure CSS demos need NO 'use client' directive because they toggle state via checkbox-hack, radio inputs, native <details> elements, or :has() — they're static SSR-safe HTML that works with JavaScript disabled. Only the 6 Light-JS demos (View Transitions API, WAI-ARIA keyboard nav, theme toggle persistence, sliding-indicator position tracking, etc.) need client-side hydration, each under 50 lines of vanilla JavaScript. Vue 3 / Nuxt 3: accepts as-is, class is native. Svelte / SvelteKit: accepts as-is; wrap in a .svelte component for scoped styles. Astro: drop into any .astro component (this site is Astro; every demo renders SSR here). Remix / Solid.js / Qwik / Lit: accepts as-is. Compared to framework-specific tab libraries: shadcn/ui Tabs is a thin wrapper on Radix Tabs (~8KB Radix + React peer dep) that gives you aria-selected + aria-controls + keyboard nav for free — but you still write all 18 visual designs yourself on top of it. @radix-ui/react-tabs is the same ~8KB primitive; provides accessibility but no visual style. @headlessui/react Tab is part of HeadlessUI's ~35KB package + React-only. @mui/material Tabs ships as part of Material UI's ~85KB base bundle + Emotion runtime + locks you into MUI theming. @chakra-ui/react Tabs requires Chakra's ~50KB Emotion runtime + full theme provider. Ant Design Tabs is part of AntD's ~200KB base bundle. Vuetify VTabs, Angular Material MatTabs — all weigh 40-100KB when tree-shaking doesn't reach the theme layer. The 18 vertical tabs in this collection total 2-6KB gzipped per demo when you cherry-pick — an entire vertical-tab system for less than a single Radix primitive. The .vt-NN numeric-prefix scoping means all 18 patterns coexist on the gallery page without a single class collision, and the same prefix guarantees you can drop any demo into a codebase that already uses .tabs, .tab, .tab-list, or .tab-panel classes without renaming a single selector. MIT licensed, no attribution required, no signup, no build step.display: none → display: block is a paint operation, not layout, but the browser optimizes it to be near-zero-cost when the panels are absolutely positioned or use CSS grid display: contents. (2) Compositor-only animations: sliding-indicator bar (Demo 05) animates via transform: translateY() on the indicator element — compositor-only, zero layout cost. NEVER animate top / height / margin — those trigger layout+paint. (3) rAF-throttled JS handlers: the 6 Light-JS demos ship idempotent IIFEs under 50 lines each. WAI-ARIA arrow-key nav (Demo 18) uses direct keydown handlers (arrow keys don't fire fast enough to need throttling). Sliding-indicator position tracking uses requestAnimationFrame when observing DOM changes via MutationObserver. (4) CLS 0 guarantee: all tab panels have explicit width AND height (or aspect-ratio) declared BEFORE any tab switch. Panels use display: none / display: block toggling, so the layout allocates ONE panel's height at a time — no layout shift when tabs change (the panel container has a consistent height across all panels via min-height or all-panels-loaded stacked layout). (5) prefers-reduced-motion tiered fallback: honors WCAG 2.3.3 AND acts as an INP protection lever for older devices. Reduced-motion drops the sliding-indicator animation (Demo 05), the View Transitions API panel-swap crossfade (Demo 14), the glassmorphism backdrop-filter (Demo 17), and any hover-scale on tab labels. The tier-1 lift: Google Ads Quality Score is derived from Core Web Vitals since 2023 — a Good INP + CLS 0 combo lowers your CPC on ad-supported pages by ~20-40% and lifts organic-search ranking on competitive keyword clusters. For a SaaS-admin site running $2-8 CPCs on productivity queries, that's real revenue. Every one of these 18 vertical tab patterns hits Good on INP + Good on CLS out of the box.32 free CSS accordions covering pure-CSS FAQ blocks with details/summary, mobile navigation menus, e-commerce filter sidebars, nested tree views, exclusive single-open groups and smoothly animated height:auto — plus 26 visual variations in vertical, horizontal and hybrid layouts. Copy-paste HTML and CSS, no JavaScript.
39 hand-coded CSS breadcrumbs — Schema.org BreadcrumbList Microdata + WCAG 2.2 accessible (Google Rich Results eligible), :has() responsive collapse, dark/light theme toggle with color-mix, Shopify/Amazon e-commerce filter chip removal, Vercel/Linear/Notion SaaS dashboard app-shell pattern, Docusaurus/Mintlify sticky scroll-shrink header, Amazon-style dropdown sibling menus, NFT/Web3 holographic shimmer (@property + conic-gradient), SaaS marketing gradient text, Airbnb filter chip with container queries, editorial blog wave underline, developer docs one-click path copy (Clipboard API), and 26 more. Semantic HTML: <nav aria-label='Breadcrumb'> + <ol> + aria-current='page'. Framework-agnostic, MIT-licensed.
26 hand-coded CSS circular and radial menu designs, from the floating-action FAB speed dial and dashboard radial hubs to game-style pie-slice wheels, SVG gooey metaball fans, hexagonal honeycombs, iris-aperture reveals, orbiting satellites on offset-path, compass-rose directional nav, sci-fi HUD rings, glassmorphism popovers built on the native Popover API, vinyl-record players, and full-takeover nebula overlays on native dialog. Every design uses CSS trigonometry (sin(), cos()) or the counter-rotation trick for positioning, ships real keyboard navigation, aria-labels on icon-only buttons, and a prefers-reduced-motion guard. Copy-paste HTML, CSS and vanilla JS with zero framework dependencies — MIT licensed.