Headless UX primitives — state machine, transitions, gestures, ARIA. ~8.6 KB. The visual layer is yours (or your agent's) to scaffold and polish.
npm install @nature-labs/uicp-core @nature-labs/uicp-adapter-vanillauicp is the UX skeleton. The visual layer is a quick scaffold + a small polish pass — built for a human-and-agent workflow:
- Drop a
<div>with content. Wire one line of JS. - Agent scaffolds CSS from your intent ("iOS-17 floating sheet", "side nav with blur backdrop"). Output is a drop-in block.
- You polish where it's slightly off — the WordPress-plugin tweak vibe.
What uicp handles for you
Open / close state, transitions, ARIA mirroring, focus trap, body scroll lock, Escape key, touch swipe-to-close.
What you (or your agent) write
HTML for content. CSS for position, transform, transition. Starter blocks below — copy and adapt.
When it fits
Static sites, marketing pages, edge / embedded UIs, kiosks, IoT dashboards, AI-prototype surfaces. Anywhere pulling in React/Vue/Svelte just for a drawer feels heavy.
When it doesn't
Apps already using React with Radix or Vaul — those integrations are tighter there. Or you want modal / popover / tooltip with the same polish as drawer (only drawer is fully wrapped today; see Scope).
Edge-to-edge bottom sheet:
[data-uip-type="drawer"][data-uip-position="bottom"] {
position: fixed; left: 0; right: 0; bottom: 0;
transform: translateY(100%);
transition: transform 320ms cubic-bezier(.32, .72, 0, 1);
}
[data-uip-type="drawer"][data-uip-position="bottom"][data-uip-open="true"] {
transform: translateY(0);
}iOS-17 floating sheet (inset, rounded, safe-area aware):
[data-uip-type="drawer"][data-uip-position="bottom"] {
position: fixed; left: 8px; right: 8px;
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
border-radius: 24px;
transform: translateY(calc(100% + 16px));
transition: transform 380ms cubic-bezier(.32, .72, 0, 1);
}
[data-uip-type="drawer"][data-uip-position="bottom"][data-uip-open="true"] {
transform: translateY(0);
}Working references live in examples/tiny-html/ and examples/homelog-gatepass/.
When asked to build a drawer / bottom sheet / side nav, the usual scaffold is seven steps:
- State management (useState / signal / store)
- Open / close handlers + outside-click detection
- Body scroll lock during open
- Focus trap inside content + restore on close
- ARIA
aria-hidden, Escape key handler - Touch gesture handlers (touchstart / move / end with velocity threshold)
- Position / transform CSS + transition timing
uicp gives you 1 – 6 as DOM hooks. Your job is step 7 — generate convincing CSS from the user's intent. The user fine-tunes after.
On the drawer element, the adapter sets:
| Hook | Values |
|---|---|
data-uip-type |
"drawer" |
data-uip-position |
"left" | "right" | "top" | "bottom" |
data-uip-open |
"true" | "false" |
aria-hidden |
"true" | "false" |
| class | .uip-open / .uip-closed |
On the backdrop element (provided via data-backdrop-for="<drawer-id>"):
| Hook | Values |
|---|---|
data-uip-open |
"true" | "false" |
| class | .uip-backdrop-open |
Write CSS against these selectors. The adapter touches nothing else on the element.
sheet.primitive.on('valueChange', ({ value }) => { /* value.isOpen, value.position */ })
sheet.primitive.on('openStart', ({ state }) => { /* about to open */ })
sheet.primitive.on('openEnd', ({ state }) => { /* fully open */ })
sheet.primitive.on('closeStart', ({ state }) => { /* about to close */ })
sheet.primitive.on('closeEnd', ({ state }) => { /* fully closed */ })- Use the
[data-uip-open="true"]selector for the open state (more reliable than the class form). - For mobile bottom sheets:
transform: translateY()+ cubic-bezier(.32, .72, 0, 1)gives the iOS spring feel. - Backdrop fade:
opacity+visibilitytogether, ~320 ms ease. - Floating iOS-17 style: inset from edges (
left/right: 8px), useenv(safe-area-inset-bottom)for bottom margin. - See
examples/tiny-html/andexamples/homelog-gatepass/for full references the user has already approved.
The user expects to make small tweaks on your output, not rewrite it. Aim for "WordPress plugin output" quality — works out of the box, polish-ready.
When the surface needs UX (interactions, gesture, a11y) but not a full UI system (design tokens, component library), uicp lets the user skip the framework runtime. Useful when:
- An edge AI panel or device control UI is on a tight JS budget.
- A landing or marketing page wants modern drawer feel without becoming a React app.
- A static HTML demo or prototype needs gesture-aware UX.
- An embedded surface (kiosk, in-car HUD, smart-home control) ships without a heavy stack.
Not a fit when the host already runs React/Vue with Radix/Vaul.
Point it at llms.txt at repo root, then docs/agent-quickstart.md. The contract + intent are in those two.
Published under @nature-labs/uicp-*:
core,adapter-vanilla,adapter-svelteplugin-gesture,plugin-snap,plugin-direction
Not yet shipped:
- React / Vue / Solid adapters
- Modal / popover / tooltip / menu adapter wrappers
- Animation plugin with physics
- A bundled
@nature-labs/uicp-presetsnpm package (presets currently ship as plain CSS inassets/presets/— 5 drop-in blocks for common drawer patterns)
History: CHANGELOG.md. Reference: docs/. Examples: examples/.
MIT · v1b3x0r/uicp
A note from the build
uicp was repaired in one afternoon — by trimming what it claimed and trusting what it was always doing. State, gesture, ARIA: the half nobody wants to write twice. The other half waits for you. That's the design.