Every component below is native HTML, works without JavaScript, and is keyboard-only walkable. Run each walkthrough with nothing but Tab, Enter, Space, Esc.
Theme:
Comparing themes? Every starter renders live, side by side, on the theme gallery.
Conformance matrix
Guarantees are inherited from native elements plus our focus handling. Proven by the walkthroughs below.
Component → WCAG level → keyboard walkthrough
Component
WCAG
Keyboard walkthrough
Button
AA
Tab to focus → Enter/Space to activate. Focus ring always visible.
Form controls
AA
Tab between controls → type → Tab past. Invalid state announced by browser.
Segmented control
AA
Native radios: Tab into the group → arrow keys move the selection. Group named by its <legend>.
Datalist
AA
Type → native suggestions appear → arrows pick. The input is a plain text field.
Dialog
AA
Focus moves in; Esc closes; focus returns to trigger. Focus is trapped.
Popover
A
Tab to trigger → Enter opens → Esc or click-away closes.
Accordion
AA
Tab between summaries → Enter toggles. One open at a time via name.
Tabs (opt-in JS)
AA
Tab into tablist → ←/→ switch, Home/End jump, Tab into the panel. Via js/tabs.js.
Carousel
A
Tab to scroller → arrow keys scroll → snaps.
Table
AA
Screen reader: headers announced via <th>. Hover affordance only.
Sortable table
AA
Header triggers are real <button>s: Tab → Enter sorts; aria-sort announces the active column and direction. Via js/table-sort.js.
Timeline
AA
A plain <ol>: reading order is the chronology, nothing interactive.
Empty state
AA
Real text content — heading + explanation + action. The glyph is decorative (aria-hidden).
Breadcrumbs
AA
Named nav landmark; current page announced via aria-current="page".
Pagination
AA
Current page is a span, not a link; links announce their labels.
Navigation
AA
Named nav landmark; Tab through links; current page announced via aria-current="page". Hamburger: toggle carries aria-expanded; Esc closes and restores focus.
Layout
AA
Sidebar splits aside + main and stacks when narrow; .bf-sticky pins while scrolling.
App shell layout
AA
Semantic landmarks (<header>, <nav>, <main>, <footer>) auto-map to grid areas; sidebar scrolls independently; main scrolls independently.
Alerts
AA
role="alert" announces errors; aria-live="polite" announces updates; dismiss button is a focusable button.
Skeleton
AA
Decorative placeholder — real content must follow; no motion under prefers-reduced-motion.
Toast
AA
role="status" announces politely; Esc or click-away closes.
Prose
AA
Heading rhythm and section spacing for long-form content via .bf-prose; nothing hidden, no ordering.
Media & avatars
AA
Images carry alt; [data-media] keeps a locked ratio; .bf-avatar is an image, announced like any other.
Avatar group
AA
The overlap is decorative — reading order and announcements are unchanged; every avatar keeps its alt.
Spinner
AA
Decorative motion stops under prefers-reduced-motion; announce progress with role="status" from your markup.
Divider
AA
The label is real text between decorative hairlines — announced like any other content.
Chip / tag
AA
The remove control is a real <button> with an aria-label naming what it removes; Tab to it → Enter/Space removes. Without JS nothing hides.
Reveal
AA
Decorative motion stops under prefers-reduced-motion; content is visible without animation. Direction variants are visual only.
Staggered reveal
AA
Sequential animation delays are visual only; content order and announcements unchanged.
Scroll-progress bar
A
Decorative position feedback; no interactive content. Live under prefers-reduced-motion.
Parallax
AA
Decorative motion stops under prefers-reduced-motion; element stays in place.
Navigation transitions
AA
Nothing interactive — navigations work identically without the transition, and content renders fully. Crossfades and morphs stop under prefers-reduced-motion.
Adaptive table (v5.0)
AA
Card-stacks when its container is narrow; still a real <table> in the a11y tree — headers announced, no markup change.
Adaptive form (v5.0)
AA
Rows reflow to a column; invalid fields summarized via :has(:user-invalid) in a live region, zero JS.
Adaptive card (v5.0)
AA
Horizontal↔vertical morph by container; heading stays a heading, link stays a link.
Generative theme (v5.0)
AA
The 12-step ramp inherits the AA-verified palette; every derived step clears a 3:1 graphical-object floor (demo/studio.html).
Typography
Heading one
Heading two
Heading three
Heading four
A paragraph. The measure is capped at --bf-content-width (64ch by default) so long text stays readable. Links like this one get a visible underline and offset.
“Barefoot: no boots, no baggage.”
Unordered list item
Another item
Ordered item
Another item
Ctrl + K for the kbd style, inline code next to it.
A separator with a centered label: add data-divider to any element that can hold text (an <hr> can't — it's void). The label stays real content; the hairlines are decorative pseudo-elements.
Section two
Content continues below the divider, exactly where you'd expect it.
Prose
.bf-prose wraps long-form content and adds heading rhythm and section spacing. The element look — blockquote border, pre, table rows — comes from the base and component layers; the wrapper only imposes the pace. The measure is still capped at --bf-content-width (64ch) so lines stay readable.
Section heading
A paragraph of long-form copy. Prose puts one beat between siblings and a full section gap before headings — each heading opens a new section and sits close to what it introduces.
“No boots, no baggage — but the words still need room to breathe.”
Tables and code keep the rhythm
Headings get a large gap above and a tight gap below; tables and code blocks get their own vertical room.
First list item
Second list item
Third list item
Fluid type steps
Token
Heading
--bf-type-md
h4
--bf-type-lg
h3
--bf-type-xl
h2
--bf-type-2xl
h1
A closing paragraph so the block doesn't end on the table.
Media & avatars
Responsive images scale down to their container (max-width: 100% + height: auto in the base layer). .bf-avatar is a circular image; [data-media] locks a ratio box (16:9 by default, data-ratio for others); .card[data-media] is a thumbnail card whose media bleeds to the top edge.
Avatars
Circular, sized from --bf-avatar-size (2.5rem); data-size="sm|lg" for other sizes. Always alt — an avatar is an image like any other.
Avatar group
Wrap avatars in .bf-avatar-group to overlap them; a surface ring keeps each face distinct. Purely visual — semantics and reading order are untouched.
Responsive images
The banner below is 2000px wide; it shrinks to its container and keeps its ratio.
Aspect-ratio embeds
[data-media] locks a ratio box on an img, video, iframe, or any element; the width follows the container and the height follows the ratio.
16:9
1:1
21:9
Thumbnail cards
.card[data-media] leads with media that bleeds to the card's top edge; the body below keeps the card padding.
Featured story
The media bleeds to the card's top edge; the body keeps the standard card padding.
Buttons
Walkthrough:Tab to a button → Enter or Space presses it. Note the visible focus ring on every stop.
Forms
Stepper
A progress tracker for multi-step flows. Native semantics; the current step gets aria-current="step". Completed steps are styled from the success token. Horizontal (default) and vertical (data-orientation="vertical") variants.
Horizontal
1Account
2Profile
3Confirm
Vertical
1Shipping
2Payment
3Review
Input groups
Wrap an input with a leading icon, currency, or unit in [data-input-group]. The affix shares the input's focus and validation states. Works with input, select, and textarea.
Date, number & email polish
Native pickers and spinners stay; Barefoot themes the surface and ensures validation states apply. Number inputs hide the spinner until hovered/focused; date inputs get a themed calendar button.
Dialog
Native <dialog>. Focus trap + Esc-to-close are built in. The triggers below use the Invoker Commands API (command/commandfor) where the engine supports it — fully declarative opening and closing; engines without it fall back to the same one native line of JS (showModal()). The Popover below is the fully JS-free alternative.
Popover (zero JS)
The Popover API: a button + popovertarget + a [popover] element. No JavaScript anywhere. In engines with anchor positioning each popover pins to its own trigger automatically — the invoker is the anchor (position-area does the placing, no inline styles needed). To pin a popover to something that isn't its invoker, give that element an anchor-name and point position-anchor at it.
The tooltip is a popover="hint": where the engine has interest invokers it appears on hover and focus and dismisses on hover-away; everywhere else popovertarget keeps click-to-show working, and engines without the hint state treat it as a plain popover. Same markup, three tiers of platform support, zero JS.
Dropdown (Popover-API menu)
A native popover menu via <button popovertarget>. Open with Enter, move with Tab, close with Esc.
All details share a name, so the browser allows only one open. For true tabs with arrow-key navigation, see the opt-in tabs below.
Why barefoot?
No boots, no baggage. Tiny, themeable, accessible by default.
Is there any JavaScript?
Zero in the framework. Native elements do the work.
How do I make it mine?
Override a few --bf-* variables. That's it.
Tabs (opt-in JS)
WAI-ARIA tabs with roving tabindex and arrow-key navigation, driven by the opt-in js/tabs.js module (zero dependencies, size in the README table). Try ←/→, Home, End.
Overview panel. Tab from the tablist lands here; arrow keys switch tabs.
Details panel — switch back and forth with the arrow keys.
Pricing panel. Home and End jump to the ends of the tablist.
Carousel (scroll-snap)
Tab to the scroller, then scroll with the arrow keys. Pure CSS. Slides are sized in container units (60cqi = 60% of the carousel's own width) — they adapt to their container, not the viewport. Add data-autoplay (opt-in js/carousel.js) to auto-advance; controls below are wired by the same module. This one carries data-progress: the bar along its bottom edge is a scroll-driven animation (animation-timeline: scroll()), so it tracks your position with no JS.
Slide 1 — press →
Slide 2 — still snapping
Slide 3 — no JS anywhere
Slide 4 — controls wired by carousel.js
Reveal (scroll-entry)
data-reveal fades and slides an element into place as it enters the viewport — a scroll-driven animation (animation-timeline: view()), not an IntersectionObserver, so scrolling back re-hides it. Direction variants: data-reveal="left|right|up|down|fade". Engines without scroll-driven animations show everything immediately; prefers-reduced-motion turns it off entirely.
Up (default)
Left
Right
Down
Fade only
Staggered reveal
data-reveal-group on a container staggers its children's reveal animations sequentially. js/reveal.js sets --bf-reveal-index on each child; without JS all children animate simultaneously.
Item 1
Item 2
Item 3
Item 4
Scroll-progress bar
data-progress on any scroll container draws a thin bar that fills as you scroll. Uses the ANONYMOUS scroll timeline pattern from the carousel. data-progress="top" pins it to the top; default is bottom.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
Parallax
data-parallax on a decorative element makes it scroll at a different speed for a subtle depth effect. Pure CSS via animation-timeline: scroll(); @supports-gated, falls back to static position.
Parallax element
Navigation transitions
Importing view-transition.css opts every same-origin navigation into a root crossfade — MPA pages get SPA-feel transitions with zero JS (@view-transition { navigation: auto }). Shared elements morph when both pages give them the same view-transition-name: the first card below is named on both sides of the link. Engines without support navigate exactly as before.
Named element
This card carries view-transition-name: bf-demo-hero. Follow the link — it flies to its twin on the pair page.
Root crossfade
Everything else crossfades; reduced motion opts back out entirely.
Set --bf-primary once and every brand token — hover, subtle, border, focus — auto-generates via oklch(from var(--bf-primary) …) behind an @supports gate. Try the live builder: Barefoot Studio →
Primary badgechroma
Change --bf-primary in Studio and these re-tint instantly — AA is checked at build time.
Icons — CSS-only
[data-icon] via mask: url() + currentColor, sized by --bf-icon-size. 0KB JS, inherits colour.
table[data-grid] extends the sticky-header table: each <th> is resize: horizontal (drag the inline-end edge). Wrap in a scroll container; stacks at ≤40rem via container query.
Project
Owner
Status
Points
Barefoot
Ada
Active
8
Heel
Lin
Blocked
5
Laces
Ken
Done
3
Container queries
The same [data-grid] markup adapts to its container, not the viewport — wrap it in .bf-contain to make the container. Narrow box below (14rem) → 1 column; full-width → 3 columns. Zero media queries.
Card A
In a 14rem container this grid stays a single column.
Card B
Same markup as the wide one.
Card C
Only the container width differs.
Card A
Wide container → three columns.
Card B
Container queries, not media queries.
Card C
Resize the container, the grid follows.
Responsive table
Add data-table="stack" and wrap the table in a query container (here .bf-contain, same as the grid above): below ~40rem (640px) rows stack as cards and the header row hides. Purely presentational — the <th> headers stay in the DOM, so screen readers still announce them.
Project status
Project
Owner
Status
Barefoot
Ada
Active
Heel
Lin
Blocked
Laces
Ken
Done
Sticky header & column
Add data-table="sticky-head" or data-table="sticky-col" — they compose ("sticky-head sticky-col") — and wrap the table in a scroll container with a bounded height/width; that scroller is what the cells stick against. Give the wrapper tabindex="0" (plus a name) so keyboard users can scroll it — tables hold no focusable content of their own. Sticky cells carry an opaque surface so scrolled rows don't show through.
Quarterly ledger
Month
Revenue
Costs
Net
Margins
Notes
Jan
12,400
8,100
4,300
35%
Launch
Feb
11,900
7,950
3,950
33%
Mar
14,200
8,400
5,800
41%
Price change
Apr
13,800
8,350
5,450
39%
May
15,600
9,100
6,500
42%
Expansion
Jun
16,100
9,300
6,800
42%
Jul
15,750
9,250
6,500
41%
Summer dip
Aug
16,400
9,400
7,000
43%
Sep
18,200
10,050
8,150
45%
Back to school
Oct
19,300
10,400
8,900
46%
Nov
22,700
11,900
10,800
48%
Holiday push
Dec
25,100
12,800
12,300
49%
Peak
Sortable table (opt-in JS)
Add data-bf-sort to a table and put a real <button> inside each sortable <th>, then load js/table-sort.js. The module reorders <tbody> rows and maintains aria-sort; numeric columns (Points below) compare numerically, text compares locale-aware. No-JS first: without the module the buttons are inert and the table stays plain but valid.
Sprint tasks
Ship segmented control
Ada
3
Audit contrast pairs
Grace
12
Draft migration notes
Lin
5
Regen visual baselines
Radia
1
Timeline
Add data-timeline to an ordered list: each entry gets a dot on a connecting spine. The chronology is the native reading order — screen readers hear a plain list.
The first run of the api.md policy: three surfaces announced, once-per-page notices.
v3.1 — platform primitives
Scroll-driven animations, hint popovers, anchor positioning — all @supports-gated.
Empty state
A class, because there is no native element: .empty-state centers a decorative glyph (aria-hidden — the heading carries the meaning), a muted explanation, and the action out of the emptiness.
☐
No projects yet
Create your first project, or import one from a template.
Cards & badges
Neutral card
A flat card with a thin border and no shadow by default.
NewPrimaryDanger
Lifted card
Add data-lifted to opt into a soft shadow.
Chips (removable tags)
An inline badge with a real <button> inside. Removal is opt-in JS (js/chips.js) — without the module nothing hides, the × just does nothing. Give each remove button an aria-label naming what it removes.
csshtmlaccessibilityzero-dependencies
Breadcrumbs
A nav with a labeled list. The current page is plain text with aria-current="page", not a link.
Pagination
The current page is a <span aria-current="page">, never a link.
Navigation
A topbar <nav> landmark: a .bf-brand, a link list, and the current page marked with aria-current="page". The row wraps on narrow screens — nothing toggles, nothing hides. Pair with .bf-skip-link as the first element in <body>.
Hamburger (opt-in JS). Add a real toggle button and an id on the list, then load js/nav.js: below 40rem of nav width the list collapses behind the button (aria-expanded tracks it, Esc closes and restores focus). No-JS first — without the module the button never renders and the list always stays visible. Resize the window under 40rem to try it.
Layout
.bf-sidebar splits a fixed-ish aside (first child, --bf-sidebar-width = 16rem) from fluid content; when the row can't fit the aside plus at least 60% main it stacks to one column. .bf-sticky pins an element to --bf-sticky-top while its scrolling ancestor moves.
Main content
Everything after the first child flows beside it. The main column grows to fill the row; when the row can't fit the sidebar plus at least 60% main, the whole split wraps to a single column — the classic sidebar pattern, zero media queries.
There is no dedicated sticky section to try; the pinned badge above only demonstrates the utility's position: sticky + --bf-sticky-top contract.
App shell layout
[data-layout="sidebar"] is a CSS Grid app shell: header, sidebar nav, main content, and footer in a named grid. Semantic elements (<header>, <nav>, <main>, <footer>) auto-map to their grid areas. The sidebar gets independent scroll via position: sticky + overflow-y: auto; main scrolls independently.
DashboardHeader
Main content
This area scrolls independently from the sidebar. The grid uses named areas: "header header" / "nav main" / "footer footer".
Semantic elements auto-map to their areas — no [data-area] needed when you use real <header>, <nav>, <main>, <footer>.
Grid variants
[data-grid="auto-fit"] flows as many columns as fit, each at least --bf-grid-min (14rem) — no container query needed, the tracks size against the row itself. data-gap="0|1…8" tunes the gap from the spacing scale.
Auto-fit
As many columns as fit, each ≥ 14rem.
Auto-fit
Resize the row and the flow follows.
Auto-fit
No container queries involved.
Tight
data-gap="2" → 0.5rem gaps.
Tight
Any 1…8 from the spacing scale.
Tight
Same auto-fit flow, tighter rhythm.
Spacing utilities
The full token scale as layout-only helpers: .bf-mt-* / .bf-mb-* (block-start/end margins), .bf-p-* (all-sides padding), .bf-px-* / .bf-py-* (inline/block padding), each mapped to --bf-space-1…8.
mt-6p-5px-3py-2
This box uses bf-mt-6 bf-p-5 bf-px-3 bf-py-2: 2rem of margin above, 0.5rem padding on the block axis and 0.75rem on the inline axis (the axis shorthands win over the all-sides padding).
Alerts & status tokens
Role-aware notices styled from the status tokens. Pick the ARIA role for the semantics — role="alert" for errors, aria-live="polite" for updates — and Barefoot paints it. Dismissible alerts pair a [data-alert-dismiss] button with the opt-in js/alert-dismiss.js module.
Deploy failed — the build timed out after 10 minutes.
Backup completed at 02:00 UTC.
New version available — see the changelog.
You're running low on storage.
Field validation
:user-invalid / :user-valid fire only after a control is touched — nothing flashes before it. Touched textual fields also draw a shape cue at the inline end (check = valid, cross = invalid); the state hue stays on the border. Under forced-colors: active the invalid border goes dashed and focus regains a real outline. Pair with aria-invalid for script-driven forms and aria-describedby for the message.
Spinner
An indeterminate loading indicator, pure CSS: mark any element with data-spinner (data-size="sm|lg" for other sizes). The motion is decorative — it freezes under prefers-reduced-motion, so pair it with text that announces progress; role comes from your markup.
Loading…
Fetching results…
Skeleton
Pure-CSS loading placeholders. Add .skeleton to any element; the shimmer is decorative and static under prefers-reduced-motion. Always replace with real content — the placeholder is never announced.
Toast
A status notice pinned to the bottom edge, built on the Popover API — declarative, JS-free. Toasts are popover="manual": they don't light-dismiss, so an app opens them when work finishes and closes them on a timer or their Close button — exactly how several stay open at once. Use role="status" for non-urgent, role="alert" for urgent. Sibling toasts stack upward: each open one lifts above the open siblings after it (pure CSS; engines without the pieces used simply overlap, newest on top). Add data-duration for auto-dismiss (opt-in js/toast.js).
Saved successfully.
Uploading assets…
Upload failed — try again.
Auto-dismissing toast (3s). Hover to pause.
Adaptive components (v5.0)
Each component senses its container, not the viewport. The same
markup renders stacked in a sidebar and full in the main column. Drag a
box's right edge to see it morph — or set data-density="compact"
on <html> to compress them all.
WCAG AA — native semantics are preserved, so keyboard and screen-reader behavior are unchanged (see the conformance table).
Table → card-stack
A data-table="adaptive" table card-stacks when its own width drops below --bf-adaptive-2. Cells carry data-label for the card captions.
Team roster
Name
Role
Location
Ada Lovelace
Engineer
London
Grace Hopper
Admiral
NYC
Katherine Johnson
Mathematician
Hampton
Segmented density
A [data-segmented][data-adaptive] compresses its labels when narrow or under data-density="compact".
Form reflow
A form[data-form="adaptive"] collapses its .bf-row to one column when narrow, and reveals an error summary via :has(:user-invalid).
Card morph
A .card[data-card="adaptive"] lays out horizontally when wide, vertically when narrow.
Quarterly report
The card flips between a side-by-side layout in wide slots and a stacked layout in narrow ones.
Tabs — scroll-snap ↔ wrap
A [data-bf-tabs][data-adaptive] keeps a single scroll-snapping row when wide and wraps to multiple rows when narrow. No .bf-contain needed — the group self-containers.
Overview content.
Pricing content.
Docs content.
Support content.
Changelog content.
Nav — drawer by container
A [data-nav="drawer"] collapses to an off-canvas drawer when its container is narrow (here a ~22rem slot) and stays an inline row in a wide slot. Reuses the same hamburger toggle.
Auto-wrap (no manual .bf-contain)
The table below is dropped straight into a plain <div> — no .bf-contain. The adaptive rule auto-establishes the container on its parent, so it still card-stacks when narrow.
Team roster (auto-wrapped)
Name
Role
Ada Lovelace
Engineer
Grace Hopper
Admiral
Verify — the framework checks your laces
The dev-only js/verify.js checker audits Barefoot's own markup contracts — the ones axe can't know — and the badge in the corner shows the live result. Break a contract and it flips ✗, with the exact fix named in the console (docs/verify.md).
Try it: click Break, watch the badge, read the console, then Fix. The checker never mutates the DOM — the stage controls do, and the badge only reports.