Skip to content

Barefoot CSS

Conformance demo

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
ComponentWCAGKeyboard walkthrough
ButtonAATab to focus → Enter/Space to activate. Focus ring always visible.
Form controlsAATab between controls → type → Tab past. Invalid state announced by browser.
Segmented controlAANative radios: Tab into the group → arrow keys move the selection. Group named by its <legend>.
DatalistAAType → native suggestions appear → arrows pick. The input is a plain text field.
DialogAAFocus moves in; Esc closes; focus returns to trigger. Focus is trapped.
PopoverATab to trigger → Enter opens → Esc or click-away closes.
AccordionAATab between summaries → Enter toggles. One open at a time via name.
Tabs (opt-in JS)AATab into tablist → ←/→ switch, Home/End jump, Tab into the panel. Via js/tabs.js.
CarouselATab to scroller → arrow keys scroll → snaps.
TableAAScreen reader: headers announced via <th>. Hover affordance only.
Sortable tableAAHeader triggers are real <button>s: Tab → Enter sorts; aria-sort announces the active column and direction. Via js/table-sort.js.
TimelineAAA plain <ol>: reading order is the chronology, nothing interactive.
Empty stateAAReal text content — heading + explanation + action. The glyph is decorative (aria-hidden).
BreadcrumbsAANamed nav landmark; current page announced via aria-current="page".
PaginationAACurrent page is a span, not a link; links announce their labels.
NavigationAANamed nav landmark; Tab through links; current page announced via aria-current="page". Hamburger: toggle carries aria-expanded; Esc closes and restores focus.
LayoutAASidebar splits aside + main and stacks when narrow; .bf-sticky pins while scrolling.
App shell layoutAASemantic landmarks (<header>, <nav>, <main>, <footer>) auto-map to grid areas; sidebar scrolls independently; main scrolls independently.
AlertsAArole="alert" announces errors; aria-live="polite" announces updates; dismiss button is a focusable button.
SkeletonAADecorative placeholder — real content must follow; no motion under prefers-reduced-motion.
ToastAArole="status" announces politely; Esc or click-away closes.
ProseAAHeading rhythm and section spacing for long-form content via .bf-prose; nothing hidden, no ordering.
Media & avatarsAAImages carry alt; [data-media] keeps a locked ratio; .bf-avatar is an image, announced like any other.
Avatar groupAAThe overlap is decorative — reading order and announcements are unchanged; every avatar keeps its alt.
SpinnerAADecorative motion stops under prefers-reduced-motion; announce progress with role="status" from your markup.
DividerAAThe label is real text between decorative hairlines — announced like any other content.
Chip / tagAAThe remove control is a real <button> with an aria-label naming what it removes; Tab to it → Enter/Space removes. Without JS nothing hides.
RevealAADecorative motion stops under prefers-reduced-motion; content is visible without animation. Direction variants are visual only.
Staggered revealAASequential animation delays are visual only; content order and announcements unchanged.
Scroll-progress barADecorative position feedback; no interactive content. Live under prefers-reduced-motion.
ParallaxAADecorative motion stops under prefers-reduced-motion; element stays in place.
Navigation transitionsAANothing interactive — navigations work identically without the transition, and content renders fully. Crossfades and morphs stop under prefers-reduced-motion.
Adaptive table (v5.0)AACard-stacks when its container is narrow; still a real <table> in the a11y tree — headers announced, no markup change.
Adaptive form (v5.0)AARows reflow to a column; invalid fields summarized via :has(:user-invalid) in a live region, zero JS.
Adaptive card (v5.0)AAHorizontal↔vertical morph by container; heading stays a heading, link stays a link.
Generative theme (v5.0)AAThe 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.”
  1. Ordered item
  2. Another item

Ctrl + K for the kbd style, inline code next to it.

/* pre + code */
@layer tokens {
  --bf-primary: #1a1a1a;
}

Divider

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
TokenHeading
--bf-type-mdh4
--bf-type-lgh3
--bf-type-xlh2
--bf-type-2xlh1

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.

Ada Grace Linus

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.

Ada Grace Linus Radia

Responsive images

The banner below is 2000px wide; it shrinks to its container and keeps its ratio.

Wide gradient banner that shrinks to its container

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.

Green field with a lighter circle
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

Preferences
View
40 60% 40% of 100GB

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

    1. 1 Account
    2. 2 Profile
    3. 3 Confirm

    Vertical

    1. 1 Shipping
    2. 2 Payment
    3. 3 Review

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.

Validated on blur — try an invalid address.

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.

Confirm deletion

This is a native dialog. Try Tab: focus is trapped inside. Press Esc to close.

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.

This is a tooltip.

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.

Accordion (one-at-a-time)

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.

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

Barefoot Chroma — One Color, Infinite Theme

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 badge chroma

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.

Command palette — zero JS

<dialog data-command> + <input type="search" list> + popover fallback. Opens with command/commandfor declaratively.

Search docs New project Toggle theme

↑↓ navigate · Enter run · Esc close

Data grid — resizable columns

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.

ProjectOwnerStatusPoints
BarefootAdaActive8
HeelLinBlocked5
LacesKenDone3

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
ProjectOwnerStatus
BarefootAdaActive
HeelLinBlocked
LacesKenDone

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
MonthRevenueCostsNetMarginsNotes
Jan12,4008,1004,30035%Launch
Feb11,9007,9503,95033%
Mar14,2008,4005,80041%Price change
Apr13,8008,3505,45039%
May15,6009,1006,50042%Expansion
Jun16,1009,3006,80042%
Jul15,7509,2506,50041%Summer dip
Aug16,4009,4007,00043%
Sep18,20010,0508,15045%Back to school
Oct19,30010,4008,90046%
Nov22,70011,90010,80048%Holiday push
Dec25,10012,80012,30049%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 controlAda3
Audit contrast pairsGrace12
Draft migration notesLin5
Regen visual baselinesRadia1

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.

  1. v3.3 — growth batch

    Segmented control, datalist skin, timeline, empty state, toast stacking, sortable tables, Sunset theme.

  2. v3.2 — deprecation wave

    The first run of the api.md policy: three surfaces announced, once-per-page notices.

  3. 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.

css html accessibility zero-dependencies

Pagination

The current page is a <span aria-current="page">, never a link.

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.

Dashboard Header

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>.

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-6 p-5 px-3 py-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.

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.

e.g. you@example.com
Digits and + only.
At least 3 characters.

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…

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
NameRoleLocation
Ada LovelaceEngineerLondon
Grace HopperAdmiralNYC
Katherine JohnsonMathematicianHampton

Segmented density

A [data-segmented][data-adaptive] compresses its labels when narrow or under data-density="compact".

View

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).

Please fix the highlighted fields.

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.

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)
NameRole
Ada LovelaceEngineer
Grace HopperAdmiral

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.