Ponytail UI
A small semantic layer for native pages: shared tokens, four color palettes, light and dark modes, and browser-native components.
The open workbench
Ponytail UI makes Bunfork pages feel mechanical, grounded, and candid. Content stays readable, controls look operable, and green remains the default signal color. Tonal surfaces carry hierarchy; decoration does not.
This is a clean-room CSS layer informed by open design systems and Ponytail's native-platform discipline. It is not a Tailwind CSS fork, does not implement Tailwind utilities, and adds no frontend build step or runtime dependency.
Native means semantics and browser behavior, not browser appearance. Every visible control is reset and redrawn with theme tokens, including text fields, select arrows, checkboxes, radio buttons, switches, file and color inputs, ranges, progress, meters, disclosure markers, popovers, and scrollbars.
Load the foundation
The bundled base layout loads the existing site styles first and the reusable foundation second:
<link rel="stylesheet" href="/assets/app.css">
<link rel="stylesheet" href="/assets/ponytail.css">
ponytail.css owns semantic --pt-* tokens and reusable .pt-* classes. Existing Bunfork variables such as --bg and --accent remain compatibility aliases. New components should consume the --pt-* names directly.
The ten .ds-* snippets in .impeccable/design.json are isolated shadow-DOM previews for design tooling. They are intentionally self-contained and are not a second runtime component API.
Themes are token scopes
The default mode follows prefers-color-scheme through native light-dark(). Set data-mode="light" or data-mode="dark" on any container for a deterministic preview. Set one palette per page with data-palette.
The component catalog adds a no-script theme laboratory. Token-skinned radio controls and :has() switch the full page between Auto, Light, Dark, Fork, Tide, Ember, and Berry states.
<main data-palette="tide">...</main>
<aside data-mode="dark" data-palette="ember">...</aside>
Semantic tokens before component values
| Group | Examples | Use |
|---|---|---|
| Canvas | --pt-color-canvas, --pt-color-surface | Page and contained surfaces |
| Ink | --pt-color-ink, --pt-color-ink-muted | Primary and supporting text |
| Structure | --pt-color-border, --pt-color-border-strong | Grouping and control boundaries |
| Action | --pt-color-accent, --pt-color-on-accent | One primary accent per scope |
| Controls | --pt-color-control, --pt-color-control-hover, --pt-color-control-disabled | Complete themed state cycle for native inputs |
| Meaning | --pt-color-success, --pt-color-warning, --pt-color-danger | Status paired with text |
| Rhythm | --pt-space-1 through --pt-space-9 | A four-pixel-based spacing ladder |
| Shape | --pt-radius-sm, --pt-radius-lg | Six to twelve pixels; pills only for compact metadata |
| Motion | --pt-duration-fast, --pt-ease-out | State feedback, never decorative delay |
| Layers | --pt-z-sticky through --pt-z-tooltip | Named stacking contexts |
Type stays local and legible
The system font stack keeps Bunfork fast and familiar. A single family uses weight and scale for hierarchy. Monospace is reserved for code, commands, keys, and tabular numbers; it is not a costume for every technical label.
- Display
clamp(2.75rem, 7vw, 5.5rem), maximum twelve characters per line where practical, letter spacing no tighter than-.04em.- Body
- At least
1rem, line height near1.65, and a target measure of70ch. - Labels
- Sentence case by default. Uppercase is reserved for genuine codes or protocol values.
- Numbers
- Use tabular figures for changing metrics, prices, and timers.
A few composition primitives
Use .pt-stack for vertical rhythm, .pt-cluster for wrapping action groups, .pt-grid for content-driven grids, and .pt-split only when two regions have a real relationship. All multi-column primitives collapse below the content breakpoint.
Bunfork's native-page CSP rejects inline presentation. Use themed attributes such as data-gap and data-width rather than a style attribute. Static framework artifacts have a separate CSP that permits the inline bootstrap code some exporters require.
<section class="pt-stack" data-gap="6">
<h2>One clear message</h2>
<p class="pt-measure">Supporting content stays readable.</p>
<div class="pt-cluster">...</div>
</section>
Cards are not the default grouping tool. Start with spacing, headings, and a divider. Add .pt-panel or .pt-card only when a bounded object needs a visible edge.
Component contract
- Start with the native element:
button,input,details,progress,table, orpopover. - Remove the user-agent visual skin, then rebuild every visible part from semantic tokens. No default checkbox, radio, select, range, file button, progress bar, details marker, dialog edge, or scrollbar ships inside Ponytail UI.
- Optional user-agent chrome such as search cancel buttons and numeric steppers stays visually suppressed; native Escape and arrow-key behavior remains available.
- Use native state attributes such as
disabled,checked,open,required,aria-current, andaria-invalid. - Use
data-variant,data-tone, ordata-paletteonly for presentation that HTML does not already express. - Keep reusable components small and focused on one semantic responsibility; compose existing primitives instead of growing mode-heavy universal components.
- Keep labels visible, error text adjacent, controls at least 44 CSS pixels high, and focus rings visible.
- Do not claim menu, combobox, tab, dialog, or toast behavior from CSS alone. Use a simpler server link or native disclosure until keyboard behavior is implemented and tested.
Open all 110 assistant concepts
The assistant catalog uses four honest coverage states: native behavior, an existing primitive, an escaped/static fallback, or a deferred row with a named database, runtime, permission, or security prerequisite. It covers observable concepts, not React or SDK APIs.
Accessibility is part of the API
- Contrast
- Normal text reaches 4.5:1 and large text 3:1. Structural control boundaries target 3:1 when the boundary identifies the control.
- Keyboard
- DOM order matches visual order. The skip link transfers focus to the programmatically focusable main landmark. Every action uses a native interactive element and receives a visible focus ring.
- Touch
- Primary controls and navigation targets use a minimum 44-pixel block size with at least eight pixels between adjacent targets.
- Motion
- Transitions explain hover, press, open, and state changes. Reduced-motion removes pulse and compresses transition time.
- Forced colors
- Controls retain visible system-color borders and focus indication.
- Language
- Logical properties and flexible widths support text expansion, CJK, and right-to-left document direction.
Pages extending the base layout can override the document_language and document_direction blocks. Shared navigation, tables, form adornments, and decorative positioning use logical inline properties so rtl changes layout direction without a second stylesheet.
Research and provenance
The component taxonomy was checked against Tailwind CSS 4, Radix, shadcn/ui, Primer, Carbon, Material Web, Bootstrap, GOV.UK Frontend, USWDS, Fluent UI, Shopify Polaris, Pico CSS, and Ponytail. Bunfork copies no upstream component source or brand assets.
Implementation choices favor Pico's semantic-element posture, government-system form clarity, Primer and Carbon density, and Radix interaction anatomy. Assistant-facing concepts were independently mapped from pinned public metadata into one Bunfork vocabulary; no source, assets, runtime API, or visual trade dress was ported. The full source matrix, pinned commits, licenses, and deferred behaviors live in docs/ui-foundation-research.md and docs/assistant-component-coverage.md.