Ui-Kit Headless Primitives
Design Log #132 — UI Kit: Headless Primitives
Written for AI agents. See Log Methodology Note below for details.
Background
Jay separates visual design (jay-html + CSS) from component logic (headless components). Most UI patterns can be built with pure HTML and CSS — modern CSS has scroll-snap, overflow: auto, :hover, details/summary, and the Popover API covers most interactive needs.
But some patterns need a thin JS shim — one or two lines that bridge between a user event and a CSS/HTML state change. These are not "components" in the traditional sense; they're micro-interactions that complete what HTML/CSS can almost do alone.
Other patterns need no JS at all — they're pure CSS/HTML techniques that designers may not know about. These are documented as skills (agent-kit guides) rather than implemented as components.
This plugin (@jay-framework/ui-kit) provides:
- Headless components for the patterns that genuinely need JS
- Designer skills (agent-kit guides) for patterns that are pure CSS/HTML
Problem
Designers building jay-html pages need common UI patterns. Some need a developer to write JS; others are achievable with pure HTML/CSS but the techniques aren't widely known:
Needs JS (headless component):
- Popover menus — Popover API requires
el.showPopover()for hover triggers - Scroll carousels — CSS scroll-snap handles snapping, but prev/next buttons need
el.scrollBy() - Copy-to-clipboard —
navigator.clipboard.writeText()is the only option
Pure CSS/HTML (skill / guide only):
- Tabs — radio inputs +
:checkedselector + CSS sibling combinators - Accordions —
<details>/<summary>with CSS transitions - Tooltips — CSS
:hover+::afterpseudo-elements or Popover API withpopovertarget(click-triggered) - Responsive navigation — checkbox toggle + CSS media queries
Design Principles
What becomes a headless component
- HTML/CSS handles 90%+ of the behavior
- The remaining JS gap is small and generic
- The component adds no DOM elements — only attaches behavior to designer-provided refs
What becomes a skill (guide)
- The pattern is achievable with pure HTML/CSS
- Designers may not know the technique
- The guide teaches the HTML structure + CSS needed, no component required
What does NOT belong
- Components with business logic (cart, search, auth)
- Components that generate markup
- Patterns that are trivial and well-known
Headless Components
1. Popover Menu (popover-menu)
HTML/CSS reality: The Popover API (popover attribute) provides light-dismiss (click outside to close), top-layer rendering, and ::backdrop. CSS :popover-open handles show/hide transitions. popovertarget works for click triggers. But hover triggers require JS — showPopover().
What the component does:
- On trigger mouseenter →
popover.showPopover() - Light-dismiss handles closing (click outside) — no JS needed for that
Contract:
name: popover-menu
tags:
- tag: trigger
type: interactive
elementType: HTMLElement
description: Element that opens the popover on hover
- tag: popover
type: interactive
elementType: HTMLElement
description: Element with popover attribute to show/hide
Designer's jay-html:
<jay:popover-menu>
<nav>
<a href="/products" ref="trigger">Products</a>
<div popover ref="popover" class="submenu">
<a href="/products/shoes">Shoes</a>
<a href="/products/bags">Bags</a>
</div>
</nav>
</jay:popover-menu>
Interactive code:
refs.trigger.onmouseenter(() => {
refs.popover.exec$((el) => el.showPopover());
});
2. Scroll Carousel (scroll-carousel)
HTML/CSS reality: CSS scroll-snap-type, overflow-x: auto, and scroll-snap-align handle snapping. CSS scroll-behavior: smooth handles animation. But prev/next buttons need container.scrollBy().
What the component does:
- Prev button →
container.scrollBy({ left: -containerWidth, behavior: 'smooth' }) - Next button →
container.scrollBy({ left: containerWidth, behavior: 'smooth' }) - Tracks
atStart/atEndfor disabling buttons
Contract:
name: scroll-carousel
tags:
- tag: container
type: interactive
elementType: HTMLElement
description: Scrollable container with overflow-x auto and scroll-snap
- tag: prev
type: interactive
elementType: HTMLButtonElement
description: Scroll to previous item
- tag: next
type: interactive
elementType: HTMLButtonElement
description: Scroll to next item
- tag: atStart
type: [data, interactive]
dataType: boolean
phase: fast+interactive
description: True when scrolled to the beginning
- tag: atEnd
type: [data, interactive]
dataType: boolean
phase: fast+interactive
description: True when scrolled to the end
Designer's jay-html:
<jay:scroll-carousel>
<div class="carousel">
<button ref="prev" disabled="atStart" class="carousel-btn prev">‹</button>
<div ref="container" class="carousel-track">
<img src="/img/1.jpg" />
<img src="/img/2.jpg" />
<img src="/img/3.jpg" />
</div>
<button ref="next" disabled="atEnd" class="carousel-btn next">›</button>
</div>
</jay:scroll-carousel>
Interactive code:
refs.prev.onclick(() => {
refs.container.exec$((el) => {
el.scrollBy({ left: -el.clientWidth, behavior: 'smooth' });
});
});
refs.next.onclick(() => {
refs.container.exec$((el) => {
el.scrollBy({ left: el.clientWidth, behavior: 'smooth' });
});
});
refs.container.exec$((el) => {
const update = () => {
setAtStart(el.scrollLeft <= 0);
setAtEnd(el.scrollLeft + el.clientWidth >= el.scrollWidth - 1);
};
el.addEventListener('scroll', update);
update();
});
3. Clipboard Copy (clipboard-copy)
What the component does:
- On button click → copy text to clipboard
- Toggle
copiedstate for visual feedback (auto-resets after 2s)
Contract:
name: clipboard-copy
tags:
- tag: text
type: data
dataType: string
phase: fast+interactive
description: Text to copy
- tag: copied
type: data
dataType: boolean
phase: fast+interactive
description: True for a short period after copying
- tag: copyBtn
type: interactive
elementType: HTMLButtonElement
description: Button that triggers the copy
Designer's jay-html:
<jay:clipboard-copy text="{shareUrl}">
<button ref="copyBtn" class="copy-btn">
<span if="!copied">Copy Link</span>
<span if="copied">Copied!</span>
</button>
</jay:clipboard-copy>
Pure CSS/HTML Skills (guides, no components)
These are documented in the plugin's agent-kit as designer skills. The plugin ships the guides; the designer follows the pattern directly in jay-html.
Tabs (radio-based)
Use hidden radio inputs + :checked + CSS sibling selectors:
<div class="tabs">
<input type="radio" name="tab" id="tab-details" checked hidden />
<input type="radio" name="tab" id="tab-reviews" hidden />
<input type="radio" name="tab" id="tab-specs" hidden />
<nav class="tab-bar">
<label for="tab-details">Details</label>
<label for="tab-reviews">Reviews</label>
<label for="tab-specs">Specs</label>
</nav>
<div class="tab-panels">
<div class="panel" id="panel-details">Details content...</div>
<div class="panel" id="panel-reviews">Reviews content...</div>
<div class="panel" id="panel-specs">Specs content...</div>
</div>
</div>
.panel {
display: none;
}
#tab-details:checked ~ .tab-panels #panel-details {
display: block;
}
#tab-reviews:checked ~ .tab-panels #panel-reviews {
display: block;
}
#tab-specs:checked ~ .tab-panels #panel-specs {
display: block;
}
Accordion (<details>)
<details>
<summary>Section Title</summary>
<div class="content">Content here...</div>
</details>
CSS transitions can animate the open/close with @starting-style (Chrome 117+).
Tooltip (CSS-only)
<span class="tooltip-trigger" data-tooltip="Helpful text">Hover me</span>
.tooltip-trigger {
position: relative;
}
.tooltip-trigger::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
opacity: 0;
transition: opacity 0.2s;
/* styling */
}
.tooltip-trigger:hover::after {
opacity: 1;
}
Plugin-Contributed Agent-Kit Guides
The ui-kit plugin ships an agent-kit/ folder with guides for both roles:
packages/plugins/ui-kit/
├── agent-kit/
│ └── designer/
│ ├── ui-kit-components.md # How to use popover-menu, scroll-carousel, clipboard-copy
│ └── css-patterns.md # Pure CSS tabs, accordions, tooltips — no component needed
These guides are merged into the project's agent-kit during jay-stack agent-kit, making the patterns discoverable for AI agents creating pages.
Questions
Q: Should this be one plugin or multiple?
A: One plugin (
@jay-framework/ui-kit). Components are tiny, guides are related. Easier to discover as a collection.Q: Should components have props for configuration (e.g., scroll amount)?
A: Start minimal — no props. Defaults should work for 90% of cases.
Q: What about accessibility (ARIA)?
A: Components set required ARIA attributes (e.g.,
aria-expandedon popover trigger). Pure CSS patterns include ARIA guidance in the skill guides.
Implementation Plan
Phase 1: Plugin scaffold + CSS skill guides
Create packages/plugins/ui-kit/ with plugin.yaml, package.json, vite.config.ts. Write the CSS pattern guides (tabs, accordion, tooltip).
Phase 2: popover-menu + scroll-carousel + clipboard-copy
Implement the three headless components. Add to fake-shop as a demonstration page.
Phase 3: Agent-kit integration
Ensure designer guides are merged during jay-stack agent-kit.
Trade-offs
- Skills vs components: Some patterns (tabs) could be either. The CSS-only approach avoids a runtime dependency but requires more designer knowledge. The skill guide bridges this gap.
- Popover API support: Chrome 114+, Firefox 125+, Safari 17+. Graceful degradation for older browsers.
- No visual output: Headless components add zero DOM. If a designer forgets a
ref, nothing happens. The agent-kit guides document required refs clearly.
Accessibility & SEO Guidelines
Each agent-kit designer guide includes accessibility and SEO instructions. The key patterns:
| Component | Accessibility approach | SEO impact |
|---|---|---|
| word-split | aria-label on container with full text, aria-hidden on spans |
No impact — text in DOM |
| letter-split | Same — critical since screen readers spell each letter | No impact — text in DOM |
| popover-menu | aria-haspopup, aria-expanded; add popovertarget for keyboard |
Menu content in DOM |
| scroll-carousel | role="region", aria-label; aria-label on prev/next buttons |
All slides in DOM |
| clipboard-copy | aria-live="polite" on "Copied!" feedback |
N/A |
| tooltip | aria-label duplicating data-tooltip (CSS ::after not in a11y tree) |
N/A |
| tabs | role="tablist", role="tab", role="tabpanel" |
All panels in DOM |
| toggle-switch | role="switch" on checkbox |
N/A |
| accordion | Natively accessible via <details> |
Content in DOM |
| click-popover | Natively accessible via Popover API | Content in DOM |
Principles
- Split text: always provide
aria-labelwith the full unsplit text andaria-hiddenon individual spans - Interactive controls: label with
aria-labelwhen visual label is an icon (prev/next arrows, copy button) - State feedback: use
aria-live="polite"for transient state changes (copied, loading) - CSS-only tooltips:
::afterpseudo-elements are invisible to assistive technology — duplicate the content inaria-label - Native HTML: prefer
<details>,popover, radio inputs — they're accessible by default - SEO: all ui-kit patterns keep content in the DOM (CSS scroll, CSS show/hide, forEach rendering) — search engines see everything
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.