UI

Plain, neutral elements and the components built on them, as Latte partials.

The ui addon holds two kinds of part. Elements are the basic building blocks, called with {ui}. Components are larger parts built on them, called with {component}. This page covers what both share: calling, setup, variants, sizes and tokens.

Elements and components

Elements are the core: the simple, common pieces every site needs, at a standard level, like a button, a card and a field. Each does one thing, and they combine by nesting. A component is a larger or more specific part built on elements, like tabs and the panel they load. Components use elements, and elements never use components.

Every param is optional and has a default. The CSS stays inside a rolling five-year browser floor: no nesting, no cascade layers, no color-mix, oklch or :has().

Calling an element

{ui button, title: 'Save' /}

{ui card, title: 'Plan'}
	{ui field, name: 'email', label: 'Email' /}
	{block footer}
		{ui button, title: 'Save' /}
	{/block}
{/ui}

{ui name} renders @ui/name.latte (a site override at site/views/@ui/ first) with only the args it is given, never the caller's params. A button inside a card never picks up the card's class or a controller's title.

An element with no content closes with /}. Content inside a paired {ui} fills the element's {block default}, and {block footer} and the like override its other blocks. That content sees the page's variables, except where the element has a param of the same name: $title inside a card is the card's.

Every element takes class, added to its base class, and attrs, any other attributes such as ['hx-get' => $url]. Both go on the root element, except on a field, where they go on the control.

Setup

A site builds its stylesheet with UnoCSS and this addon's preset. The preset brings the reset, the tokens, the element CSS and the size rules, then the site's own utilities after them:

import { defineConfig } from 'unocss';
import ui from '../../phlat/addons/ui/assets/preset.mjs';

export default defineConfig({
	presets: [ui()],
});

The layout loads that stylesheet and p('@ui')->file('ui.js') for the small shared accessibility script. It closes an open details.dropdown on Escape or a click outside it, and has nothing to re-initialise after an HTMX swap.

Variants and sizes

A variant is a look that never changes by breakpoint, so it is an attribute: data-variant="outline", set by the variant param. Components write it as .btn:where([data-variant=outline]), which keeps the rule as weak as a utility class, so a one-off utility still wins.

A size can change by breakpoint, so it has both spellings, from one scale in preset.mjs: data-size="lg" (the size param) and the class btn-lg, which takes responsive prefixes (lg:btn-lg). Use the param by default and the class only when it changes by breakpoint.

Every other data attribute is UI state.

Tokens

Token names (--background, --primary, --primary-foreground, --muted-foreground, --border, --ring, --radius and the rest) hold HSL channels: --primary: 0 0% 9%. Alpha is hsl(var(--primary) / 0.9), and utilities get the same names: bg-primary, text-muted-foreground, rounded-lg (from --radius). Dark values apply under [data-theme=dark], or with the system preference unless [data-theme=light] is set.

Each element and component reads its own variables (--btn-h, --btn-bg and so on), and its variants and sizes only set them, so a site restyles one by setting its variables.