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.
Dropdown (details)
AA
Enter toggles → Tab through items. (Esc-close is browser-dependent — use the popover menu for guaranteed Esc.)
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.
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.
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 (details/summary)
A native <details data-menu>. 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 an element up into place as it enters the viewport — a scroll-driven animation (animation-timeline: view()), not an IntersectionObserver, so scrolling back re-hides it. Engines without scroll-driven animations show everything immediately; prefers-reduced-motion turns it off entirely.
Each card rises 1rem…
…and fades from 0 to 1…
…as it scrolls into view.
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
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.
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. 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).