UI components index
This is a one-page index of every UI component the framework ships.
The canonical reference for any given component is its live demo
page at /components/<slug> plus the Go doc comments on the
component's Config / constructor. Those stay current automatically
because each release lands the demo + the code together.
Where to look for what
| You want… | Look at |
|---|---|
| Live behavior (click it, drag it) | Run the website: ./scripts/dev-watch.sh → /components/<slug> |
| Constructor signature + every field | go doc github.com/DonaldMurillo/gofastr/framework/ui.<Name> |
| Full-page composition choice | gofastr docs ui-composition-recipes |
| Widget presets (Modal, Drawer, …) | go doc github.com/DonaldMurillo/gofastr/core-ui/widget/preset |
| Runtime data-cui-* attributes | runtime-contract |
| What's coming / deferred | ROADMAP.md §2 |
The website's components index page lists every component with a
one-line description (examples/site/components.go
is the source of truth). The examples/site test suite
keeps every registered /components/<slug> paired with:
- at least one chromedp e2e test
- zero axe-core violations against the default WCAG 2.0/2.1 A/AA rule set
- at least one
*_test.gofor the package that defines it
Catalog
Live demos at http://localhost:8082/components/<slug> once the
dev server is up.
Variant-taking components panic on unknown variants; apps extend the
sets with ui.RegisterButtonVariant / RegisterButtonSize /
RegisterCardVariant / RegisterStatusVariant (one status
registration covers StatusBadge, Tag, Callout, and Notification). See
"Custom variants on framework components" in ui-getting-started.
A host that receives the variant as a string (kiln's node renderer,
the generated resource screens, uihost's trusted node renderer)
resolves it with ui.ParseButtonVariant, which knows the four built-in
variants and every one the app registered through
ui.RegisterButtonVariant, and answers ok=false for any other
spelling so the host applies its own default instead of panicking.
Every component Config carries ExtraAttrs html.Attrs, forwarded to
the component's root element for data-* test hooks, analytics
markers, and ARIA overrides. On components following the sanitized
contract, component-owned keys (class, id, data-cui-*,
behavior-critical attributes) are dropped; a legacy set still forwards
raw (enumerated in framework/ui/extraattrs_contract_test.go). See
"Attribute pass-through" in ui-getting-started.
Primitives & semantic markup
- kbd:
core-ui/html.Kbd, semantic<kbd>for keyboard input - shortcuthint:
framework/ui.ShortcutHint, OS-aware chord chips (⌘ on Mac / Ctrl elsewhere) - avatar:
framework/ui.Avatar, circular avatar with image → initials fallback (sm/md/lg/xl);Squarerounds it to the medium radius for a logo tile, and--ui-avatar-bg/--ui-avatar-fgrecolour the initials surface - avatargroup:
framework/ui.AvatarGroup, readable 10% overlap, compact corner presence dots, and an adaptive-surface overflow chip - thumbnail:
framework/ui.Thumbnail, a square lazily loaded image preview (sm 2rem / md 4rem / lg 8rem,object-fit: cover); aSrcthe image URL policy refuses draws nothing, so the caller draws its own empty mark - icon:
framework/ui.Icon, 291 vendored Feather and Lucide drawings and 13 name aliases fromframework/ui/icons. Existing site, navigation, and toolbar names resolve through the same catalog.currentColorstroke;AriaLabelopts intorole="img". See Icons. - languagemenu:
framework/ui.LanguageMenu, the language switcher for an app built withframework.WithLocalePrefixes: a Menu with a globe trigger naming the current language, and one radio row per catalog locale linking this page in that language (rel="alternate" hreflang). Each language is named in itself (i18n.LanguageName;Namesoverrides). The runtime retargets the rows after each client-side navigation, so it can sit in persistent chrome. Renders nothing without path locales onCtx - link:
framework/ui.Link, typed anchor with external-link affordances + unsafe-scheme href sanitizing - muted:
framework/ui.Muted, subdued inline<span>for secondary text - inline code:
framework/ui.InlineCode, a short escaped<code>in the mono font on the soft surface, for an operator, a field name or an example inside text;InlineCodeDangerdraws it in the danger tone, for an error a program returned (a job's last error); blocks of code useCodeBlock
Buttons & form controls
- button:
framework/ui.Button, semantic button with typed variants (primary / secondary / danger / ghost) + sizes, rendered through the headless structure with thefui-buttonclass map.Disabledrenders the disabled state (adisabledkey inExtraAttrspanics pointing at the field). Everydata-cui-*key inExtraAttrsis runtime wiring and travels the typedActionseam; a key outside the vocabulary panics naming it — under the old carrier contract it rendered as a dead attribute. The admitted vocabulary:data-cui-rpc,-rpc-method,-rpc-body,-rpc-signal,-rpc-navigate,-rpc-open,-rpc-close,-rpc-reset,-rpc-after-text,-rpc-after-disable,-rpc-scroll-to,-rpc-refresh(re-poll a widget after success),-confirm,-signal-set,-signal-inc,-signal-toggle,-push-state,-open,-deeplink,-toast,-pane-open,-pane-key(a keyed pane's deep-link identity),-pane-close,-prefetch,-intercept-close— the shapesinteractive.Action.Attrs()/OpenOnClick/PaneKeyand friends produce; every otherdata-cui-*key belongs to the component that renders its own markup for itIcondraws a registered icon before the label, andIconOnlythe icon alone in a square button with the label as its accessible name.Shortcut("Mod+S", needsID) binds a chord that clicks the button, drawn after the label as ShortcutHint chips and announced througharia-keyshortcuts.QuietUntilDirtydraws the button in the secondary look until the form it submits has unsaved edits (the leave guard'sdata-hui-dirty) - linkbutton:
framework/ui.LinkButton, anchor styled as a Button, for CTAs that navigate.IconOnlydraws theIconalone, as Button does.Externalownstarget/rel(noopener). Carries the link-legal wiring keys (data-cui-push-state,-prefetch,-open,-deeplink, the intercept marks, anddata-cui-nav="off"for a whole document load); every otherdata-cui-*key is refused, as always — a link navigates, a button acts - toggle:
framework/ui.Checkbox/Radio/Switch, labelled native inputs rendered throughheadless.Choice/headless.Switch— one inline run, the label wrapping the control; FieldErrors-aware, an error wrapping the run and its message in one unit - checkboxgroup:
framework/ui.CheckboxGroup/RadioGroup,headless.Groupfieldset of checkboxes / radios; the group's message (error, else hint) belongs to the group, never to each leaf - changelist:
framework/ui.ChangeList, what an edit changed, one row per field: the field's label, the old value struck in a<del>, an arrow, the new value in an<ins>; screen readers hear "from" and "to" (i18nui.ChangeFrom/ChangeTo) in place of the arrow, and an empty side reads asEmptyValue. Values arerender.HTML, so a caller passes the same cell markup its table draws.ui.Ago(ctx, now, t)beside it says a past time the way an activity feed does ("just now", "5m ago", "2h ago", "3d ago"), throughi18nui.AgoNow,AgoMinutes,AgoHoursandAgoDays - colorfield:
framework/ui.ColorField, colour swatch beside a text input holding the same value, as one control (headless.Color's affix shell); the text input is the source of truth and requires aName, so values the native picker cannot represent (transparent,var(--x)) survive and mark the shell instead of degrading — the swatch falls back to black; the headless module keeps the swatch and the text one value for every value the picker can show (the short#abcform expands for the swatch and is pickable, not an error). UseColorPickerwhen a swatch + label is enough - segmented:
framework/ui.SegmentedControl, radio-group styled as a sliding pill bar; an emptySelectedchecks the first segment, and aSelectedno option lists checks none (the pill hides), so the group submits nothing rather than a value nobody chose. Labels target option-indexed input IDs, so values such asC++andC#remain independently selectable. - counter:
framework/ui.Counter, signal-driven numeric counter with +/− buttons - signaltoggle:
framework/ui.SignalToggle,role="switch"button bound to a boolean signal - toggleaction:
framework/ui.ToggleAction, three-state commit/untoggle button (idle → pending → committed) with optional mutex groups; a toggle button whose accessible name isIdleLabelin both states (aria-pressedcarries the state, only the visible label flips), so keep the idle label's words inCommittedLabel - passwordinput:
framework/ui.PasswordInput, password field with a reveal button rendered throughheadless.Password— a toggle button: its name ("Show password") and word ("Show") never change,aria-pressedsays whether the password shows and the kit paints the pressed state; the headless behaviour module binds the reveal and its words come fromui.StringsFor; a control, not a field: the error belongs to the FormField around it - searchinput:
framework/ui.SearchInput, search field with icon prefix + clear button; the clear button tracks the value after input and native form reset, including a server-prefilled value - searchresults:
framework/ui.SearchResults, a full search results page (GET form, match count, results with section links, pagination) that works without script - inputgroup:
framework/ui.InputGroup, input with prepend / append addons - rating:
framework/ui.RatingInput, 1-N star/heart with Size / Gap / Shape / Icon knobs; the radios are in reading order (1 to Max in the DOM), so arrows move the way the stars read, and the checked and hover fill is:has()in the kit sheet - rating-display:
framework/ui.Rating(ui.RatingDisplayConfig), a read-only score drawn as glyphs sized to themselves, onerole="img"with a localized "4 out of 5" name. Shares Shape / Size and the--ui-rating-colorknob with RatingInput. Use it for a testimonial or a product average, where a disabled RatingInput would announce a form control and space its stars on 44px tap targets. - slider:
framework/ui.Slider,<input type=range>with optional live value mirror - rangeslider:
framework/ui.RangeSlider, dual-thumb range with cross-clamp;ValueLow/ValueHighare*int, nil the unset end (low onMin, high onMax), so a set pair ofnew(0),new(0)renders as 0 to 0 rather than the whole range - numberinput:
framework/ui.NumberInput, number field with explicit +/- buttons - textarea:
framework/ui.TextArea, multi-line input with typed Autogrow and Monospace (the mono font token, for JSON and code) and JSON (checks the text as JSON as it is typed: an invalid value reads aria-invalid and stops the form's submit with "Enter valid JSON", throughheadless.Textarea'sJSONsentence and thedata-hui-jsonhook of the headless-controls module; empty text is left to Required, and the server still validates) - colorpicker:
framework/ui.ColorPicker, styled native<input type=color> - timepicker:
framework/ui.TimePicker, styled native<input type=time> - datepicker:
framework/ui.DatePicker, a calendar that stays on the page beside the native<input type=date>it fills, for a range in one control, refused days, or notes on days; the native input is the value and posts without script, theheadless-datepickermodule owns the date-grid keyboard, and a month change is an island the page serves (ui.DatePickerMonth). An ordinary date staysDateField - select:
framework/ui.Select, labelled native<select>with help, error, placeholder, and required marker;SelectConfig.Selectedsupplies a stored value and, when nonempty, overrides option-levelSelectedflags. A stored value absent fromOptionsremains selected as an extra option labelled with that value, so saving does not silently replace retired choices. WhenSelectedis empty, the last flagged option wins, except that a flagged empty value selects the placeholder whenPlaceholderis set. With no flagged option, the placeholder or browser's first option applies.Actiondraws a control after the select on its row (an open-record link);LabelHiddenkeeps the label for assistive tech and hides it from view - taginput:
framework/ui.TagInput, free-form chips, Enter/comma to commit, Backspace to remove - multiselect:
framework/ui.MultiSelect, checkbox group inside a disclosure with a chips strip above; submits as a plain form (the field name repeats per checked option, no script needed), the chips are the enhancement theheadless-multiselectmodule rebuilds from the checkboxes' own state - form:
framework/ui.Form, opinionated<form>wrapper with submit + error summary;Widelifts the readable-measure cap so a FormFrame body can split into its side column - formfield:
framework/ui.FormField, labelled input with required + help + error states - control:
framework/ui.Control, styled native input (email, password, datetime-local, file, tel, url, search…) for a FormField builder - textfield:
framework/ui.TextField, typed labelled native text field with required, help, error, autocomplete, and length attributes;LabelHidden(also on NumberField, DateField and DateTimeField) keeps the label for assistive tech and hides it from view - numberfield:
framework/ui.NumberField, typed labelled native number field with explicit min/max/step bounds - datefield:
framework/ui.DateField, typed labelled native date field with HTML-date min/max bounds - datetimefield:
framework/ui.DateTimeField, typed labelled native date-and-time field (datetime-local) with min/max bounds and a step in seconds - formsection:
framework/ui.FormSection, grouped fields with a shared heading + description - formframe:
framework/ui.FormFrame, record form in two columns — a wide main column beside a narrow side rail (one of three namedSideWidthsizes, each a modifier class reading a--ui-form-frame-side-*token) that drops under the main one below 48rem of the form's own width, a container query, so a drawer on a wide screen stacks and a full page sits side by side; the main column stops at 45rem, andSidePanelmakes the rail a bordered panel that sticks beside the fields (under aContentRow{Sticky: true}toolbar it stops below the bar, offset by the--ui-sticky-topthe row publishes, which already counts aStickyHeaderpage's header) - validationsummary:
framework/ui.ValidationSummary, inline summary of form validation errors - conditionalfield:
framework/ui.ConditionalField, form region visible on first paint and hidden by the runtime until the watched field matches — one value (WhenValue) or any of a list (WhenValues) - formrepeater:
framework/ui.FormRepeater, dynamic list of repeating field groups (add / remove rows); each row's Remove is a danger kit button and Add a secondary one (thefui-buttonclasses, so the theme's danger treatment and the button density apply), and the items list carries no list bullets or indent - repeater:
framework/ui.Repeater, dynamic add / remove item list with min / max limits; each row's Remove is a ghost kit button and Add a secondary one (thefui-buttonclasses, so they match every other button on the page), and the items list carries no list bullets or indent - stepwizard:
framework/ui.StepWizard, multi-step form with a progress indicator bar
Selection & input composition
Pass the page or island render context as Ctx to TextField,
NumberField, DateField, DateTimeField, Select, and FormRepeater.
Their derived IDs then share the render ID scope, so same-named controls
keep distinct labels and hint/error targets. Explicit ID values remain
caller-owned and reserve their dependent IDs; callers must keep explicit
IDs unique. Without an ID scope, the original name-derived spelling stays.
ToggleConfig.Ctx, RadioGroupConfig.Ctx, and CheckboxGroupConfig.Ctx
claim the same scoped ID families. Group leaf IDs use the option index,
not the value's slug, so C, C++, and C# remain distinct. Submit
values are unchanged. Explicit group IDs reserve both descriptions and
all indexed leaf IDs; standalone radio IDs disambiguate within Ctx.
Generated entity-form checkboxes forward the same render context, including
when an explicit control ID is supplied.
- combobox:
framework/ui.Combobox, typed combobox over the headless primitive: a labelled input withrole=comboboxwiring, a GET-form no-script fallback to the same endpoint, an RPC-driven listbox whose options are real links, and a status region the module announces results through;core-ui/patterns/comboboxis retired - inline edit:
framework/ui.InlineEdit, a value edited where it is shown (a table cell): the value is the trigger of a dismissable<details>editor laid over it: the caller'sControl(give it a hidden label,LabelHidden, since the cell names it) and Save on one line of aui.Form. Saving sends it toActionover the runtime's form RPC (PUT, orPATCHwithMethod), toasts Saved and navigates toReturn, so the server redraws the page with the stored value; Escape or a press outside closes it unsaved.Labelis the trigger's accessible name. The value tints on hover and shows a focus ring. - segmented links:
framework/ui.SegmentedLinks, a strip of links drawn as a segmented control, oneCurrent(aria-current="true") inside a labelledrole="group": a list's Table / Cards switch. Every segment is a plain link, so the choice lives in the URL;IconOnlykeeps each segment's text as its accessible name and title.Countdraws a muted figure after a segment's text (the Jobs page's "Failed 3"). The strip keeps its own width in a flex column and scrolls inside itself when wider than its box. - shortcut sheet:
framework/ui.ShortcutSheet, the keyboard help sheet: returns a trigger snapshot and a Modal preset likeCommandPalette. Mount the preset once, then renderui.ShortcutSheetTrigger(ctx, cfg)in each request's chrome so its default visible text and accessible label follow that reader's locale.ctxtakes precedence overcfg.Ctx; nil uses English fallbacks, and an explicitTriggerLabelstill wins. KeepNameandShortcutconsistent with the mount. The trigger opens it on its own chord (?by default) and is a plain link toFallbackHrefwithout script. The modal listsItems(a chord and what it does) as keycaps beside each label under a heading that names the dialog, with a close button. MountShortcutSheetFuncfor translated rows and renderShortcutSheetTrigger(ctx, build(ctx))beside it.ui.ShortcutList(ctx, items)draws the same rows for the fallback page. The list search inui.FilterToolbartakes a chord too:FilterSearch.Shortcut(entity lists use/) focuses it from anywhere outside a text field, with a keycap at the input's end while it is empty. - column picker:
framework/ui.ColumnPicker, a list's Columns dropdown: one row per column in display order, each a link that shows or hides it (ToggleHref; aLockedcolumn has none), and beside a shown one, move-earlier and move-later links (UpHref,DownHref; empty draws the control disabled), then a Reset link. Every control is a navigation the caller builds, so the order lives in the list's URL and works without script. - filter rows:
framework/ui.FilterRows, structured filter rows inside a list's GET filter form: each row a field select, an operator select and a value, submitted as three repeated params (FieldName,OpName,ValueName) in row order, with visually hidden numbered labels ("Field 1"). The rows a filter holds come first, then one blank row to add another; a row with an empty value is ignored, which removes it without script. On a narrow container the value drops below the two selects. - picker:
framework/ui.Picker, a form field that picks one record out of many: a search input over a server-searched listbox (headless.Comboboxin Pick mode) and a hidden input that submits the pickedValue. The search input is detached from the host form (itsformattribute names no form), so only the value submits.Endpointanswers each search, a POST of{"q": …}, withui.PickerRows(id, options, more);Optionsare the first rows shown on focus andMorea note row after them ("Showing 20 of 312"). Focus selects the shown label so typing searches afresh; typed text that was never picked goes back to the picked label on blur, and an emptied input clears the value.Actionsits after the input (an open or a New button).Formnames
the form the hidden value submits with (the HTML form attribute), for
a picker drawn outside its form; headless.ComboboxPick.Form is the
same field on the primitive. headless.ComboboxRows renders the rows for any island combobox answer.
- commandpalette:
framework/ui.CommandPalette, ⌘K modal + combobox composition - globalsearch:
framework/ui.GlobalSearch, sticky inline/-shortcut search bar - dropzone:
framework/ui.FileDropzone, hero file-drop surface with image previews - fileupload:
framework/ui.FileUpload, drag-drop file picker over native<input type=file>
Navigation
- recordsummary:
framework/ui.RecordSummary, compact dominant record or event summary with status, next-decision, balanced phone metrics, a bounded support rail, ownership, and a lead-region natural-width action that stays early on phones - skiplink:
framework/ui.SkipLink, focus-visible bypass link for jumping to main content - pageheader:
framework/ui.PageHeader, top-of-page header with title / eyebrow / subtitle / actions - anchoredrail:
framework/ui.AnchoredRail, sticky in-page nav rail with scrollspy-tracked active state - contentrow:
framework/ui.ContentRow, the page's content row — a start column (the nav landmarkContentRowConfig.NavLabelnames, wrapping aui.Sidebarwhose own inner nav names only the links list), the main column, an optional toolbar band above main beside the nav (a region landmarkToolbarLabelnames), and an optional context aside that releases its width around an empty outlet.Breakpointpicks the stack width (below md by default, below lg to matchSidebarConfig.DrawerBreakpoint);Viewport: truefills the rest of the viewport below the header and scrolls each column on its own — the row reads the header's height from--size-header-heightand cannot style its parent, so recipes pair it with a page-tallui.Stack{Screen: true}and the fixed header band;Sticky: truekeeps the window as the scroller and pins the nav column at--ui-sticky-top, under the header when the page stack setsStickyHeader, so a long menu scrolls on its own beside the page. - tabs-signal:
framework/ui.Tabs, signal-driven tab strip (click sets the signal; CSS shows the panel);StateAttrsaddsdata-state=active/inactiveto the buttons,IDwiresaria-controls/idpairs,VacateHiddenships hidden panels empty with their content in a JSON stash (restored on show by theheadless-tabsmodule) so page-scoped test locators cannot match hidden text — the contract knobs a port needs, each off by default - tabnav:
framework/ui.TabNav, a navigation strip of links where at most one is current (aria-current="page"; a secondCurrentpanics): the view tabs above a list, the sub-navigation of a record screen. Every tab is a plain<a>, so a tab click is a navigation the client router intercepts and the URL stays shareable — the query-param contract since list state moved off islands.Badgerides inside the link,aria-hidden, for a view's row count.Enddraws a control at the strip's trailing edge, outside the<nav>landmark, such as a "Save view" dropdown; the root is then a<div>holding both. Distinct fromtabs-signal, which swaps panels in place under one URL - breadcrumbs:
framework/ui.Breadcrumbs,<nav aria-label>landmark over an ordered trail; the last step (or the one carryingCurrent) isaria-current="page"text, never a link to itself, and separators arearia-hiddenCompactMobile: trueshows the final two steps below md and hides the leading separator; the full trail remains on desktop. - pagination:
framework/ui.Pagination, numeric page pager with typed query props (Path,Query url.Values,PageParamdefaultp) — every href is built throughnet/urlwith the page parameter replaced, never a%dpattern;Windowsizes the page neighbourhood,OmitPrevNextdrops the ends. The Island is optional, the Table posture: a list screen's page anchors are plain navigations the client router intercepts, and a pager inside an island region (aDataTablefooter) carries the RPC contract beside its hrefs and thedata-hui-pagehook the table module restores focus through.core-ui/patterns/paginationis retired: this is the only pager - sidebar:
framework/ui.Sidebar, responsive primary nav with persistent, collapsible (local-storage persisted), off-canvas, and auto-hide variants;Collapsemoves the collapsed state to the server,GroupMarkupswaps<details>groups forbutton[aria-expanded][aria-controls]+hiddencontainer,CollapseLabel/ExpandLabelrename the toggle andCollapseTextits visible foot label (the toggle is a sticky row at the column's foot, icon-only in the rail),SectionLabelsdraws group headers as small uppercase section labels over flush links,RaisedCurrentdraws the current page's link as a raised pill (surface, hairline ring, small shadow; the admin's nav), and--ui-sidebar-row-heightsets a row's height (2.25rem under a fine pointer, the 44px touch target on a coarse one) (see Sidebar: server-owned collapse state); in a column of fixed height (aContentRow{Sticky: true}nav) the inline sidebar fills it and its nav scrolls in its own region, so the brand stays put and the collapse row sits under the links instead of over the last one; setNavLabelwhen a page has multiple navigation landmarks and mount the matching drawer withMountSidebar - sidebarbrand:
framework/ui.SidebarBrand, the product mark at a sidebar's head: a square logo tile (theLogoimage, or the name's initial on the inverted surface), the name, and an optional mutedSubline;--ui-sidebar-brand-tilesizes the tile. A collapsed rail keeps aPrependthat is only a brand and draws its tile alone; any otherPrependhides there - sidebardrawertrigger:
framework/ui.SidebarDrawerTrigger, the sidebar's drawer toggle rendered on its own — the relocated hamburger a header row carries while the sidebar itself lives in the body; pass the SAMESidebarConfigtheui.Sidebarrender uses and the pair stays one logical control (the trigger hides itself at >= md exactly like the sidebar's inline copy, soSuppressDrawerTriggerand a relocated trigger never draw two) - menu:
framework/ui.Menu, keyboard-driven dropdown built on<details>;MenuItem.IDgives a row an addressableid(caller-owned uniqueness, ignored on separators;ExtraAttrsstill cannot setid);MenuConfig.TriggerElementswaps the framework<summary>for a caller-owned trigger — pass the inline HTML of your own<button>(or<a>) and the runtime makes it the controller: it wiresaria-haspopup/aria-controls/aria-expandedat hydration, toggles on click/Enter/Space (activation is prevented — put navigation on menu items), focuses the first menuitem on open, and returns focus to your element on Escape. Use it whenever the page owns the trigger's markup or classes (avatar buttons, pill buttons): routing such an element throughTriggerHTMLnests it inside the summary, which axe reports asnested-interactive.TriggerElementoverridesLabelandTriggerHTML; give each trigger menu a distinctIDwhen two structurally identical ones share a page.MenuConfig.LazyPanel: truekeeps the panel's rows out of the document tree until the menu is first opened: SSR ships them inside an inert<template data-cui-menu-lazy>as the panel's only child (the panel<div>stays, soaria-controlsstill resolves), and the runtime mounts them on first open — before its focus-on-open lookup, so the keyboard contract is unchanged. Use it when live-DOM queries must not see closed-menu rows: host Playwright contracts that pingetByText('Theme')to the first visible match orgetByLabelto exactly one element. The rows are still in the HTML source, so this hides nothing from a crawler that parses the response. The cost: rows are not in the DOM until first open, so host JS that binds menu rows by id at page load must use delegated listeners instead, and with JavaScript disabled the menu opens empty (only the disclosure module mounts the rows); the zero value renders rows inline exactly as before.MenuConfig.IconOnly: truedraws the trigger as themoreicon withLabelkept as its visually hidden accessible name, for a table row's action menu.MenuConfig.Avatardraws the trigger as that avatar (round, borderless, 32px, a 44px box on coarse pointers) withLabelas its hidden name, for an account menu; it panics besideTriggerHTML,TriggerElementorIconOnly.MenuConfig.Icondraws a registered icon beforeLabelon the default trigger (a list's Columns menu); it panics on an unregistered name and besideTriggerHTML,TriggerElement,IconOnlyorAvatar. A row's action is one ofHref,RPC,Action,Do(a built*interactive.Action, so the row carries its confirm, navigate and toasts) orCopy(&ui.MenuCopy{Target: "<element id>", Toast: "Copied"}copies that element's text through the headless copy module); a row naming two panics at render.MenuItem.Check: truemakes a row a checkbox option (role="menuitemcheckbox",aria-checkedfromChecked): the state is the server's, so pair it withHreforRPCthat toggles it, as a list's Columns menu does; it panics besideRadioorChildren. ADisabledrow is out of the keyboard order and inert to the pointer; a disabledHrefrow renders with nohref, so neither a keyboard Enter, an assistive-tech activation nor a crawler follows it. - tree:
framework/ui.Tree, WAI-ARIA treeview on the headless primitive: roving tabindex, arrows/Home/End/type-ahead bound by the registeredheadless-treemodule; leaf hrefs are real anchors, a static branch is real markup, and aLazyPathbranch keeps the kernel's rpc wiring on its toggle with a hidden signal-bound group (LazySignalPrefixnames the signal); a lazy branch starts collapsed even withExpandedset, so its first toggle loads it - toc:
framework/ui.TableOfContents, a server-rendered contents nav from theItemsyou pass, marking the section in view whenTargetis set;PhoneSheetfolds it into a bottom sheet a floating pill opens on phones - steprail:
framework/ui.StepRail, vertical numbered step rail with an active step + anchor links; it sticks--ui-step-rail-top(24px) below--ui-sticky-top, so it stops under aStickyHeaderpage's header - steps:
framework/ui.ProgressSteps, linear step indicator (horizontal + vertical)
Disclosure / surface widgets
- collapsible:
framework/ui.Collapsible, styled<details>with clickable summary + Escape-to-close - actionlist:
framework/ui.ActionList, a short list of rows drawn as a menu's rows (full width, a touch target tall, the soft surface on hover and focus), each a link (Href) or a button carrying a built RPC (Do), with an optional registeredIconandDanger. It is the rows of a panel that also holds other controls, such as the admin's accountDropdownwith its theme switches: aMenupanel isrole=menuand may hold only menu rows, so a mixed panel's rows are a plain list Tab walks. - columnpicker:
framework/ui.ColumnPicker, a list's Columns dropdown: one row per column (ColumnChoice) with a show/hide link and up/down links, aLockedcolumn that cannot be hidden, and an optional reset link. The choice lives in the list's URL. ColumnPicker, ActionList and SegmentedLinks apply the shared anchor URL policy to every destination; refused URLs keep their label or icon as a disabled span with no href. - filterrows:
framework/ui.FilterRows, rows of field, operator and value selects/inputs (FieldName,OpName,ValueNamename the repeated query parameters) that a list's filter form submits. - segmentedlinks:
framework/ui.SegmentedLinks, a strip of links drawn as a segmented control, each with an optionalCountandIcon; theCurrentone carriesaria-current. A filter that is a link per value, with no Apply. - dropdown:
framework/ui.Dropdown, a trigger button whose panel floats under it and holds any content (a list's filter fields, a "save view" form), where aMenuholds only command rows. A native<details>(headlessDisclosurewithDismiss): it opens with no script and closes on a click outside, Escape, or a client-side navigation.Iconnames a registered icon,Countdraws a badge (the filters applied),Align: ui.DropdownEndlines the panel up with the trigger's end edge, andAvatardraws the trigger as that avatar alone (the label kept as its accessible name; not besideIconorCount), an account panel in an app bar. Fields inside a closed dropdown still submit with their form. Knobs:--ui-dropdown-min-width,--ui-dropdown-max-width; the viewport bounds both. - modal:
core-ui/widget/preset.Modal, focus-trapped dialog with deeplink - drawer:
core-ui/widget/preset.Drawer, edge-mounted sliding panel - bottomsheet:
core-ui/widget/preset.BottomSheet, bottom-anchored Drawer variant - popover:
core-ui/widget/preset.Popover, click-triggered floating surface - floatingpanel:
core-ui/widget/preset.FloatingPanel, corner-anchored persistent panel - tooltip:
framework/ui.Tooltip, CSS-only hover/focus reveal - toast:
core-ui/widget/preset.ToastStack, client-side slide-in notifications (no SSE, no server queue) - notificationbell:
framework/ui.NotificationBell, bell + unread badge + popover dropdown. The trigger is an anchor toHref; its accessible name keeps the requiredLabelbefore the localized unread count, such asNotifications: 0 unread notifications.SignalUnreadchanges the count without dropping that label prefix. Mount the constructor's popover once, then renderui.NotificationBellTrigger(ctx, cfg)in each request's header. Its explicit context resolves both the initial spoken count and the live-update sentence template;cfg.Ctxonly resolves the snapshot returned by the constructor. Nil uses English fallbacks. KeepNameconsistent with the mount, and build any translated callerLabelwith that request's context before passing it to the trigger. The mounted body's default empty text uses its own request context; explicitLabelandEmptyTextremain as supplied. - confirmaction:
framework/ui.ConfirmAction, trigger + alertdialog Modal - commandpalette: (also under Selection; same component)
Layout & display
- layout:
framework/ui.Stack/Cluster/Grid/Center/Spacer/Box;Clusterwraps by default and exposes the explicitNoWrapopt-out;Shrinklets a no-wrap row narrow, its last child (a trail that ellipsizes) taking the squeeze while the earlier children keep their size.StackConfig.Screen: truemakes the stack the page column: at least one viewport tall (100dvh) with its last child pushed to the bottom — the option a recipe uses to keep a short page's footer at the bottom of the screen.StackConfig.StickyHeader: truepins the stack's<header>child and publishes its height (--size-header-height) as--ui-sticky-top, the offset every top-sticky part below reads (ContentRow's Sticky frame, StepRail, SectionMenu, TableOfContents, AnchoredRail, GlobalSearch, NetworkRetryBanner, a top-edge Sticky, FormFrame's side panel, an anchored Section); a region that scrolls on its own (a bounded Box, a pinned nav column, a ListDetail list) resets it to 0.BoxConfig.Bounded: truecaps a Box at the--ui-box-bounded-sizeknob (20rem) and scrolls what overflows: a window for a long log or list in a panel, or for previewing a viewport-height component (Workbench) inside a page - listdetail:
framework/ui.ListDetail, a labelled, keyboard-scrollable list beside a detail slot. Keep it in the group layout to preserve its node and scroll position while detail navigation swaps the primary slot. On phones the detail stacks above the list.ExtraAttrsaccepts the two markers returned byLayoutTree.VTRegion()for whole-region transitions. - listdetailplaceholder:
framework/ui.ListDetailPlaceholder, marks an unselected detail screen forListDetailConfig.MobileSinglePane. This opt-in phone mode shows the list on an index and the detail on an issue, including a missing issue; desktop remains split.BackHref(withBackLabel, default "Back") adds a link to the list at the top of the detail pane, shown only while a phone shows the detail alone; it requiresMobileSinglePane. The default ListDetail still stacks both regions on phones. - container:
framework/ui.Container, max-width page wrapper with breakpoint padding;Width: ContainerPagecaps at the page measure (--size-page-width, 66rem) — the editorial column a contained page's main and its header/footer bands share. Like the bands, its content box is the measure and the page gutter (--size-page-gutter, clamp(20px, 5vw, 32px)) sits outside it, so main's text starts on the header brand's edge at every width;Padis the page's block rhythm:ContainerPadPagepads under the header (--ui-container-pad-start, clamp(40px, 6vw, 64px)) and above the footer (--ui-container-pad-end, clamp(48px, 7vw, 80px)),ContainerPadEndonly above the footer; a padded container is page-level, so it takes the page gutter inline at everyWidth(an unpadded one keeps the nested-container steps, 8/16/24px), except inside an app frame's content area (ui.ContentRowwith aSidebar), whose own padding is the gutter; an unknown value panics;Start: truepins the column to the inline start and drops its gutter, for a measure inside a frame that already pads its content (a settings page in an app shell's pane) - section:
framework/ui.Section, labelled content section with heading + description;Overlinedraws the heading as a small upper-case muted group label over a run of cards (a dashboard's "Billing"), still the section's h2;Bandgives a section with no eyebrow the page-band treatment (display-size heading). With noID, the id is the heading's slug; setCtx(the screen's render context) and a second section with the same heading on the page getsfeatures-2(itsaria-labelledbyfollows), while an explicitIDis never changed. The page render's id scope comes fromapp.WithRequest;render.UniqueIDhands ids out from it, and the derived ids ofui.Tabsandheadless.Carouseluse it too (a carousel slide's ownIDis reserved like any explicit id).ui.Carousel,ui.Repeaterandui.Countergiven noIDtake theirs from the scope when theirCtxcarries one (render.HasIDScope), so the id inside an island is the same on the page and in the RPC answer; with no scope they fall back to a process-wide counter. An id is taken with the ids built from it (a section'sfeatures-titleheading id, a tab strip's-tab-0and-panel-0), so a section titled "Features title" next to one titled "Features" getsfeatures-title-2. The app renders each page answer as one id region (render.NewRegionBatch): every explicit id on the page, whether its component calledrender.ReserveIDor only wrote it in anidattribute (RegionBatch.ReserveRenderedreads the page for them), is reserved before a derived id is handed out, soID: "features"keeps its id even below a section titled "Features", which becomesfeatures-2. Layout outlet fills render concurrently but their ids are settled in outlet order afterwards, so the output never depends on which fill finished first. A part of the page that also renders alone takes an id namespace (render.WithIDNamespace): its derived ids read<namespace>--<id>, the same inside the page and alone, and apart from every other id on the page. A deferred outlet (OutletOptions.Deferred), which a part request fills, uses its address, marked so no island id reaches it (_l-shell-aside--featuresforl:shell#aside); an island rendered withisland.NewIsland(id, c).RenderCtx(ctx)uses its id (cart--features), so render it withRenderCtxon the page and in its RPC handler. A namespace keeps letters, digits and single inner hyphens and spells any other byte as_and two hex digits (islandcart.v2givescart_2ev2--features; outletaside-ais_l-l_3ashell_23aside-a--features), so no two namespaces give one id and no island id reaches an attribute unescaped. Inside its namespace an island or deferred outlet is one id region of its own, so an explicit id there wins over a derived one too. A widget's chrome renders without the page's scope on the page and on its/chromefetch alike, so it keeps the ids it derives. - responsive:
framework/ui.Responsive, viewport-swap pair (independent desktop / mobile variants).Belowpicks the breakpoint the mobile variant shows below:ui.StackBelowMD(the zero value, 48rem) orui.StackBelowLG(64rem) - panehost:
framework/ui.PaneHost, primary pane + openable secondary/tertiary side panes with a responsive overlay-drawer collapse - themed:
framework/ui.Themed, wraps a subtree in a registered section-level theme override - workbench:
framework/ui.Workbench, viewport-height inspector shell: a fixed-width rail that scrolls on its own beside a pane that fills the rest (an<iframe>in the pane fills it edge to edge); stacks below 720px - card:
framework/ui.Card, a surface with header/body/footer overheadless.Card;Hrefmakes the whole card one link;Actionputs a control at the header's end, level with the heading (a feed's "View all" link), and panics besideHref; a card with anIDnames its heading<ID>-title, so a chart in the card can pass that asLabelledByAn interactive card witharia-current="page"receives the active surface treatment, including marks applied by the runtime's navigation sweep. - sticky:
framework/ui.Sticky, theme-token sticky wrapper for top or bottom edge pinning;Offsetmeasures from the pinned edge (a bottom bar sits that far above the viewport's bottom), and a top-edge wrapper draws its hairline only while stuck, in browsers that report the stuck state (container-type: scroll-state). An unknownEdgeorOffsetpanics - aspectratio:
framework/ui.AspectRatio, CLS-safe aspect-ratio wrapper for media and embeds;Ratiois required and an empty or unknown bucket panics, since a box with no ratio collapses its child to 0px - image:
framework/ui.OptimizedImage, responsive<picture>with CLS-safe Width/Height - pipelineimage:
framework/ui.PipelineImage, multi-format<picture>consumingframework/imageVariantSet output (typed sources + LQIP/BlurHash) - divider:
framework/ui.Divider, semantic separator (horizontal, vertical, labelled) - gallery:
framework/ui.Gallery, Grid / Strip / Masonry thumbnail surface - lightbox:
framework/ui.Lightbox, zoom-overlay modal; pairs with Gallery - carousel:
framework/ui.Carousel, horizontal scroll-snap slider - sortablelist:
framework/ui.SortableList, drag-and-drop + keyboard reorderable list on the headless primitive (Space grabs, arrows move, columns following the screen in RTL, Space drops, Esc, focus leaving the row or a new grab cancels; a drag released off the list puts the row back); the commit is the pattern's server round trip — 2xx confirms, non-2xx reverts, linked columns shareGroup, a versioned 409 refetches fresh rows (SortableListItemsis that fragment) - optimisticaction:
framework/ui.OptimisticAction, button that flips to its SSR-declared success state on click; the RPC fires underneath and rolls back with a shake on non-2xx - toggleaction:
framework/ui.ToggleAction, OptimisticAction's three-state cousin: idle ↔ committed with optional untoggle endpoint andGroupmutex (committing one reverts its siblings) - networkretrybanner:
framework/ui.NetworkRetryBanner, persistent banner that shows on RPC-failure threshold or SSE silence; retry button pings a health endpoint to recover
Data display
- metricband:
framework/ui.MetricBand, flat semantic signal band (one row wide, two columns on phones) for related facts that should not become a wall of cards;Hintadds a trend or qualifier - datatable:
framework/ui.DataTable, sortable / paginated / island-swappable rows; a sort header is an anchor in both postures — the URL is a list screen's state, and an embedded table's anchors carry the island contract beside their hrefs. Every sortable header shows a muted up-down glyph, and the sorted one an up or down arrow, drawn from itsaria-sort;--ui-data-table-sort-icon-sizesizes it. A cell holds its value on one line, so a table wider than its box scrolls inside it;Column.Wraplets a prose column wrap, at least--ui-data-table-wrap-width(16rem) wide.Column.Fitshrinks a column to its content (a checkbox, a row menu) so the others take the spare width; a fitted column cannot also Wrap.Column.Truncateholds a long value (an error, a URL) to one line of at most--ui-data-table-truncate-width(20rem), cut with an ellipsis; it cannot combine with Wrap or Fit.Column.SelectAllnames the row checkboxes a header checkbox governs: it checks and clears that table's boxes of the name and shows mixed when only some are checked; an empty table draws no box. A sort header's label sits level with the values under it.Responsive: ui.ResponsiveCardsturns each row into a labelled card below 640px of container;Responsive: ui.ResponsiveRowsturns it into a two-line phone row below 720px: theColumn.Phoneslots place cells (PhoneTitleoverPhoneSubtitle,PhoneMetaoverPhoneDetailat the end,PhoneLeadbefore for a checkbox,PhoneEndafter for a row menu), a column with no slot is not drawn on the phone, and one column must be thePhoneTitle; a row with noPhoneLeadcell keeps no empty lead column. In both modes the header row is clipped on a phone and drawn while one of its controls (a sort link, the select-all box) holds focus, so a keyboard user sees where focus is and can still sort.Flushdrops the table's frame, for rows inside a card. Under the rows,Summary(the window sentence, "1–25 of 40", also appended to the sort announcement) sits on the left andFooterTools(a rows-per-page menu) before the pager on the right;Noteis a line of small print below them. - selection:
framework/ui.Selection, aBarthat acts on selected rows over theBodythat holds them, a list's bulk-action form over its table. The bar shows only while a checkbox in the body is checked, through CSS:has()with no script; a browser without:has()shows it always. Body checkboxes join the bar's form withform=.Floatingdraws the bar after the rows as an inverse pill held to the bottom of the screen, with "{n} selected" (ui.selection.count, kept by the headless behaviour) and, withForm, a reset button that clears the rows (ui.selection.clear); a table's select-all box never counts, nor does a box in a form inside the body (a cell's inline editor).Copy(a same-origin URL that answers CSV for repeated_idparameters) adds a Copy CSV button: the headless behaviour fetches it with the checked rows' ids, writes the answer to the clipboard and toastsui.selection.copied(orui.selection.copyFailed); a URL on another origin is ignored. Built onheadless.Selection. Knob:--ui-selection-gap - statcard:
framework/ui.StatCard, metric card with label/value/trend; a value too long for its card wraps inside it instead of running out of it.Hreflinks the label to what the number counts,Iconnames a registered icon for the card's head, andActionputs a control (a "New" LinkButton) in the head beside it, outside the link, and wraps under a long label.Plaindrops the card's frame for a figure inside aui.StatStrip.Tiledraws theIconin a tinted square at the head's start, theActionat its end and the label on the line below (the admin dashboard's entity cards). A 4-card dashboard row lives in aui.Grid; the Grid defaultMin: "16rem"wraps 3+1 inside a sidebar-narrowed content column (~900px). For a 4-up row that fits (and degrades to 2+2 on tablet), passGrid(GridConfig{Min: "13rem"}, …); theMinknob is the intended control, not a Grid default change (16rem stays right for general content cards). A card fills its grid cell, so a row of cards shares one height even when each sits in a wrapper (a polled region);GridConfig.Fillkeeps a lone card, or a short last row, at a column's width instead of stretching across the row. - animatedcounter:
framework/ui.AnimatedCounter, an arrival animation driven byrequestAnimationFrame, not visibility or an intersection observer. It renders the figure alone, with no step buttons (headless.CounterProps.Display): a number that animates in is read, not operated. The signal owns the number: a live value differing from the arrival target stops the walk before its first frame or between frames, even when it matches the number already painted. Reduced motion leaves the SSR figure untouched, and rescanning the same markup never restarts the animation - entrylist:
framework/ui.EntryList, a dense list of links to read: eachEntrya title link, itsSource(a domain) muted beside it, and aMetaline of caller markup under it (points, author, age, a link to the discussion).Rankeddraws an<ol>numbered fromStart, so page two of a thirty-entry list reads 31, 32, …; the numbers are the browser's list markers, muted and tabular. A visited title turns muted.Labelis required. Knobs:--ui-entry-list-gap,--ui-entry-list-rank-width - commentthread:
framework/ui.CommentThread, a discussion with replies nested as deep as it goes: eachCommentis an<article>(aByline, then theBody, both caller markup) in a list item, itsRepliesa list inside that item, so assistive tech hears depth as list nesting. Each level is indented a step and ruled on its leading edge; in a container under 30rem the step shrinks so a deep thread keeps a readable column on a phone. Long words and URLs in a body wrap.Labelis required. No script. Knobs:--ui-comment-thread-gap,--ui-comment-thread-indent,--ui-comment-thread-indent-compact - timeline:
framework/ui.Timeline, vertical event rail; each event has aTitleor, in its place, aLead: headline markup the caller draws (a bold name, a link) from escaped parts;Whenplus an RFC 3339Machinetimestamp emits semantic<time datetime>(invalid pairs panic);Whenalone is a span, andMetastays an independent caption;TimelineEvent.Icondraws a registered icon in the marker, a bordered circle (--ui-timeline-icon-size, 28px) with the icon tinted by the variant - sparkline:
framework/ui.Sparkline, pure-SVG inline trend chart - piechart:
framework/ui.PieChart, SVG ratio chart (donut variant via InnerRadius); a zero or non-finite total uses the localized shared chart empty state, retaining the chart's ID, class, accessible name and safe extra attributes - gauge:
framework/ui.Gauge, one reading against a range as a semicircle SVG dial: CPU at 73%, latency against its budget, disk used. The root is an ARIAmeternamed byLabeland carryingaria-valuemin,aria-valuemax,aria-valuenowandaria-valuetext(ValueText, the reading as people read it: "73%", "412 ms"), so a screen reader announces the reading.Min/Maxdefault to 0–100. A reading outside the range pins the arc to the nearer end whileValueTextstill says what was read; NaN or Inf renders the empty state.Bandscolor the arc by where the reading falls: the band with the highestFromat or below the value wins (GaugeSuccessfrom 0,GaugeWarningfrom 70,GaugeDangerfrom 90); no bands takesGaugePrimary. The reading's text scales withSize. An unknown tone, an inverted range, a span too wide to compute or a missingLabelpanics. No script. A fraction of a task that finishes isProgress, not a gauge. - scatter chart:
framework/ui.ScatterChart, points on two numeric axes for the question a bar chart can't answer: do two measures move together (seats against MRR, latency against payload size)? EachScatterSerieshas aName(required) andPointsof{X, Y, Label}, drawn in one color: a palette token, a registered status variant or a CSS color, falling back to the palette for anything else. Both axes are always drawn, rounded out to clean steps of 1, 2, 2.5 or 5 times a power of ten, with optionalXLabel/YLabeltitles and aShowLegendstrip that wraps at the right edge. Tick labels carry as many decimals as the step needs (past 1e9, or under a step of 1e-9, as many significant digits), and the left gutter grows to fit them. Each point's<title>reads "Series: Label (x, y)". A NaN or Inf coordinate is skipped; with no finite point the chart renders the empty state.LabelledBynames it for assistive tech, as with the other charts. No script. - heatmap:
framework/ui.Heatmap, a grid of values shaded by size: sign-ups by weekday and hour, errors by service and day.RowsandColslabel the sides andValuesholds one row oflen(Cols)values per row label; a mismatch panics. A row label past 17 characters is cut with an ellipsis, and a grid whose cells would fall under 4px wide widens pastWidthrather than clipping. Each cell takes one of four strengths of one paletteColor(primary by default), from the lowest value to the highest, and its<title>reads "Row, Col: value", withFormatwriting the value. A NaN cell draws empty and says "no data"; a grid with no reading renders the empty state.ShowLegendadds a "Less … More" strip. Column labels thin out when they would overlap. No script. - barchart:
framework/ui.BarChart, categorical SVG bar chart. Legible by default: value labels ride above every bar cap (opt out withHideValues), the y-scale rounds up to a clean maximum so uniform / near-equal data keeps visible headroom (no wall of full-height slabs), a hairline baseline grounds the bars, and longShowLabelscategory labels wrap onto two lines (a single over-long word ellipsizes, full text preserved in the bar's<title>).FitHeightsizes the SVG to hug the tallest bar (a 96px cap) instead of the fixedHeight, so no blank band pads the space above the caps — bar ratios stay identical, only the padding goes.ShowAxisadds left value-axis ticks + gridlines. Per-barColoraccepts a palette token (primary/info/success/warning/danger), a registered status variant name, or a hex/rgb/hsl/oklch/var() CSS color; any other value falls back to the theme primary. Axes andFitHeightremain finite for finite readings, including subnormal values and values near the float64 limit. When padded headroom cannot be represented, the axis uses the data maximum. - linechart:
framework/ui.LineChart, multi-series time-series chart with area + legend. Edge x-axis labels anchor inward so the first/last tick don't clip against the SVG boundary. SmallHeightvalues expand to fit the label and legend gutters plus at least an 8px plot, rather than clipping the plot above the viewBox. Legend entries wrap atWidth, count Unicode runes rather than UTF-8 bytes, and reserve a row of height for each line. - codeblock:
framework/ui.CodeBlock, styled<pre><code>sample block; theHighlightLinesfunc pre-tokenizes lines for syntax highlighting, and theHighlightLines []LineRangefield (fence{1,3-5}),Diff,HighlightWords, andWrapadd line bands, diff marking, word marks, and soft wrapping — see CodeBlock: line highlighting, diffs, wrapping - codetabs:
framework/ui.CodeTabs, the same snippet in several languages (Go / TypeScript / curl …) behind a zero-JS tab strip; pure composition ofheadless.Tabs+CodeBlockwith copy buttons. Selection is per-tabset, not a page-wide language preference. The SDK docs site (framework/sdkdocs) is the flagship consumer. - counter:
framework/ui.Counter, numeric counter with +/− buttons mutating a client-side signal - jsonviewer:
framework/ui.JSONViewer, collapsible tree of arbitrary values - diffviewer:
framework/ui.DiffViewer, unified or split diff renderer. File headers are recognized outside hunk ranges only, so removed--comments and added++expressions remain changed code in either mode; consecutive files may carry their own header pairs. A final line terminator creates no extra row; explicit blank context lines remain visible. LF and CRLF patches render identically: one trailing carriage return per physical line is removed, never carriage returns inside its content. PassCtxfor localized split-column defaults (i18nui.DiffOld/DiffNew, keysui.diff.old/ui.diff.new); nil falls back to English, and explicitLeftLabel/RightLabeloverride translations. - markdown:
framework/ui.Markdown, themed wrapper overcore/markdown.Measure: truecaps the line length at a reading width (--ui-markdown-measure, 72ch) for long-form pages in a wide column; generated markdown blocks set it. Fence options reach it through thedata-metaattributecore/markdownemits and forward ontoCodeBlockConfig:title=,showLineNumbers,scroll,{1,3-5}/highlight=,diff,words=, andwrap(see the CodeBlock section for the table). Unknown options are ignored, so an option added later degrades to a plain block rather than breaking one, and the raw info string stays on the block root indata-meta - factbox:
framework/ui.FactBox, single labelled fact (compact label + value pair; label-first or value-first) - detaillist:
framework/ui.DetailList, label/value description list for record detail views; stacks labels above values in narrow containers, including desktop context cards, and on phones;Stackeddraws a read-only value in a form, label above and the value boxed like a control;Spreaddraws compact side-column facts, label at the start and value at the end, a long first value (an id) truncating ahead of a copy button - progress:
framework/ui.Progress, native<progress>with theme styling; determinate (Value0..Max, clamped at render) or indeterminate (Value< 0), labelled visibly (ShowLabel) or througharia-label. A named-stage walk isProgressSteps, a different component - terminalblock:
framework/ui.TerminalBlock, terminal transcript with a labelled header andTerminalOut/TerminalOK/TerminalErrlines - skeleton:
framework/ui.SkeletonCard/SkeletonRow/SkeletonAvatar/SkeletonTimeline/SkeletonLine, loading placeholders overframework/headless.Skeleton: hidden shimmer bars, one polite "Loading…" announcement per preset; their own stylesheet visually hides that accessible announcement even without the host's app.css floor, so it occupies no grid cell; the timeline preset draws event-shaped rows (dot, name line, two text lines) matching whatui.Timelinearrivals look like, andSkeletonLinedraws ONE capped bar — the loading twin of a breadcrumb trail or a one-line label (an area'sLoading.Show, a crumbs skeleton) - spinner:
framework/ui.Spinner, inline CSS loading indicator
Tags, badges, filters
- tag:
framework/ui.Tag, interactive pill (linked / removable / status-variant) - statstrip:
framework/ui.StatStrip, one frame around one to six figures divided by hairlines, a dashboard's headline numbers as one band. EachCellsentry is the caller's markup (a poll region holding aStatCardwithPlain, which drops the card's own frame); two columns on a narrow screen, an odd last figure spanning the row.Labelnames the group - statuspill:
framework/ui.StatusPill, compact status pill with optional leading dot (neutral / accent tone) - statusbadge:
framework/ui.StatusBadge, small inline pill conveying state (success / warning / danger / info / neutral);Dotdraws a filled circle in the label's tone before the word, the status-column shape, sized by--ui-badge-dot-size(0.375rem) - filtertoolbar:
framework/ui.FilterToolbar, the filter/sort control strip above a list (facet<select>or radio-pill groups + search + sort + Apply/Reset), a single URL-driven GET form; wraps → stacks responsively so nothing clips on mobile.Dropdown: truemoves everything but the search into a Filters dropdown,Extraadds controls of the same form,Appliedadds to the Filters badge, andToolsdraws links and link menus at the row's end - filterchipbar:
framework/ui.FilterChipBar,role=toolbarof removable filter chips; a chip's × POSTs itsDismissBodytoDismissPathand the bar swaps in the response (the × is a link toDismissPathwithout script).ClearAllPathand SegmentedControl'sRPCPathmust pass the anchor URL policy and start with a single/; unsafe or cross-origin paths panic at render instead of leaving a dead RPC or an executable fallback link. - shortid:
framework/ui.ShortID, a long identifier (a UUID, a job id, a hash) in a table cell: its firstLengthcharacters (default 8) in monospace with an ellipsis, the full value as their title, and an inline copy button that copies all of it. Screen readers hear the full value once, from a visually hidden copy the button targets. A value no longer thanLengthshows whole - copybutton:
framework/ui.CopyButton, clipboard button with SR-announced confirmation;IconOnlywithIcondraws a registered icon ("copy","link","arrow-up-right") in place of the glyph, and a labelled button withIcondraws it before the label;Inlinedraws it quiet (no border or fill until hover, a 24px target) for a copy control inside a line of text;Ghostdraws it like a small ghost iconButton, for a row of icon buttons - drawerbar:
framework/ui.DrawerBar, the bar across the top of an intercepted drawer: a close button (data-cui-intercept-close), the layer's path in mono, a copy-link button whenCopyURLis set, an open-as-page link (data-cui-intercept-page) whenPageURLis set, thenActions.PrevandNextdraw up and down icon links after the path, each markeddata-cui-intercept-swapso the target renders in the same drawer; an empty one draws disabled, and both empty draw neither. It sticks to the layer's top edge while the layer scrolls. Draw it whenapp.OverlayFromContextreports a drawer - toolbar:
framework/ui.Toolbar,role=toolbarwrapper for grouped actions
Status & banners
The OptimizedImage and PipelineImage showcase API links target
framework/ui, where the components live; their image processing helpers
remain in the separate framework/image package.
The OptimizedImage gallery uses an in-memory PNG dashboard mockup with matching intrinsic dimensions. It needs no separate static image asset.
The LanguageMenu gallery shows disabled language choices because the docs
site does not register locale-prefixed pages. A live ui.LanguageMenu reads
the host's real locale configuration from its request context.
The docs site mounts the sidebar drawer on both the Sidebar and SidebarDrawerTrigger showcase pages, including their mobile trigger previews.
The Form, FormRepeater, Repeater and ConditionalField gallery previews submit to the docs site's no-op POST endpoint. They demonstrate native form submission, not sign-in or persistent row mutations; use example values rather than real credentials.
The FilterChipBar gallery is a static preview: its dismiss and Clear all controls use the docs site's no-op POST endpoint rather than a GET-only page. Hosts supply their own state-changing endpoints and bound response region.
The gallery's Tag dismissal is a live island on /components/tag.
gallery.TagDemo(dismissed) renders its seed or dismissed state; the docs
site serves that fragment on POST and reads ?dismissed=beta for no-script GETs.
Bind the initial region once with interactive.BindHTML; replacement
fragments do not repeat the response-signal binding inside that region.
- themetoggle:
framework/ui.ThemeToggle, dark/light/auto toggle that persists color-scheme mode; fresh scaffolds mount the adaptiveframework/ui/theme.Default()palette, while app-owned themes must keepDarkColorscomplete;Fillstretches the pill across its container, the options sharing the width - themepicker:
framework/ui.ThemePicker, switches the whole page between the app theme and registered overrides (style.RegisterThemeOverride) by putting the override class on<html>; the choice persists per browser (see theming → "Page themes");Fillstretches its pill asThemeToggle's does - backtotop:
framework/ui.BackToTop, fixed scroll affordance that appears after a threshold - banner:
framework/ui.Banner, page-level persistent status strip - callout:
framework/ui.Callout, persistent inline info / warning / danger / neutral block - notification:
framework/ui.Notification, toast-styled inline notification (variant + dismiss) - emptystate:
framework/ui.EmptyState, centered title + description + optional CTA for no-data screens;Compactdraws one muted line in a small panel, for an empty list inside another screen - signout:
framework/ui.SignOut, logout control: minimal form POSTing to the auth sign-out endpoint; compatible withauth.WithBFFPosture, whose logout handler enforces same-origin submission - pollingindicator:
framework/ui.PollingIndicator, pulsing dot + label confirming a polling RPC is firing - seo:
core-ui/seo+uihost.WithSitemap/WithRobots+ScreenCanonical/ScreenHreflangs/ScreenSchema, per-page SEO + sitewide sitemap.xml / robots.txt - seo-bundle:
ScreenSEO(ctx)returning anSEOstruct, per-screen bundle of description + canonical + hreflangs + robots + OG + Twitter Card + JSON-LD in one declaration; alternative to the per-method calls above
Marketing & page sections
- hero:
framework/ui.Hero, centered landing hero (eyebrow + title + subtitle + actions + optional media);Ledetakes inline markup in place ofSubtitle,Footerholds content under the actions (an install line, a byline, stats) - herosplit:
framework/ui.HeroSplit, two-column hero (copy + media) with equal / copy-wide / media-wide ratios; name the section withAriaLabel, or withHeadingID(the id of the h1 in Copy, wired asaria-labelledby): a heading inside a section does not name it - pricingcard:
framework/ui.PricingCard, plan tile (price + period + feature list + CTA), optional featured highlight; cards placed directly in aui.Gridthat holds only plans share their row lines, so prices and feature lists start level across a row; a card in a wrapper cell or a mixed grid keeps its own rows - authcard:
framework/ui.AuthCard, centered card shell for login / register / reset forms (title + alert + notice + body + footer)
Sidebar: server-owned collapse state
A sidebar group has Children and no Href: its label opens and closes
the group. Put the section's overview page in the group's first child
link instead. Setting both Children and Href panics with this fix.
SidebarConfig.CurrentPath already splits who decides the active item:
set it and the server renders the highlight, leave it empty and the
runtime stamps it after hydration. Collapse does the same for the
collapsible rail.
The zero value (SidebarCollapseAuto) keeps the localStorage behaviour:
the runtime restores the collapsed state after hydration and persists
every toggle. SidebarCollapseCollapsed and SidebarCollapseExpanded
make the server own the state: the collapsed rail (or expanded column)
ships in the SSR bytes as data-collapsed on the root, the toggle
button renders aria-expanded="false" plus the expand label when
collapsed, and the runtime neither reads nor writes localStorage for
that sidebar.
Use it when the collapse preference is per-user server data — a setting restored from the database that must survive first paint on a device whose localStorage says otherwise:
// pref is the signed-in user's stored sidebar preference. collapse := ui.SidebarCollapseAuto switch pref.Sidebar { case "collapsed": collapse = ui.SidebarCollapseCollapsed case "expanded": collapse = ui.SidebarCollapseExpanded } sbCfg := ui.SidebarConfig{ Title: "myapp", Variant: ui.SidebarCollapsible, Collapse: collapse, CollapseLabel: "Collapse sidebar", ExpandLabel: "Expand sidebar", Items: items, }
Three more knobs round out the contract surface:
CollapseLabel/ExpandLabelrename the toggle button. The button carries the expand label andaria-expanded="false"while collapsed, the collapse label andaria-expanded="true"while expanded; both names ride along as data attributes so client-side toggles keep the custom wording. Defaults: "Collapse navigation" / "Expand navigation".GroupMarkup: ui.SidebarGroupButtonrenders groups (items withChildren) asbutton[aria-expanded][aria-controls]plus a container that carrieshiddenwhen closed, instead of the default<details><summary>. The sidebar runtime module toggles both on click. For hosts whose specs pin that markup shape. Two<details>behaviours do not carry over: group open state is not persisted across navigation (every swap resets a group to its server-rendered state; the details dialect carriesdata-hui-disclosure-persist), and the dialect needs JavaScript — without the runtime module a closed group's links are unreachable, while<details>opens natively with JS off.Variant: ui.SidebarAutoHiderenders the persistent column as a 64px icon rail at >= md viewports and reveals the full column on hover or keyboard focus (:focus-within), straight from the component's stylesheet — no JavaScript, no host CSS.Prependis a component rendered between the title and the<nav>on every body path: the inline column,SidebarBody, and theMountSidebardrawer. A docs site whose phone header hides its section tabs puts the section<select>here, so the drawer (the only navigation at that width) carries it without forking the mount. It takes acomponent.Componentrather than HTML becauseMountSidebarruns once at boot while the drawer body renders per request: a Prepend that implementscomponent.ContextComponentsees the request on the inline and drawer paths alike, so the select can mark the current section. Wrap static markup inapp.NewStaticComponent. It hides with the title in the collapsed rail and the auto-hide rest state;Footerstays below the nav.ui.MountSidebarFunc(r, build)mounts the drawer from a builder instead of a value: the drawer body callsbuild(ctx)on every chrome request, so a sidebar the layout already builds per request (ui.Sidebar(build(ctx)), a footer with the signed-in user'sui.SignOut) shows the same footer and items in the phone drawer.MountSidebar(r, cfg)renderscfgas given, so a config built once at boot withcontext.Background()gives every phone user the anonymous footer. The drawer's name, variant and header brand come frombuild(context.Background())at mount time.- The mounted drawer is a
role="dialog"named "Navigation" in the request's locale (theui.sidebar.drawerphrase,i18nui.SidebarDrawerLabel):aria-labelledbypoints at a hidden span with the id<DrawerName>-labelin the drawer header, rendered whether or not the header shows a brand, so a screen reader never announces a bare "dialog". Countends a link's row in a small muted figure (render.Text("12")); the collapsed rail hides it. In a kept shell, hand in an inline route area (app.AreaSpec{Inline: true},l.RouteArea) so the figure re-renders with each client navigation; a plain value keeps the figure the page loaded with.MatchPathrides the rendered link asdata-cui-match-prefix(the value, not the href, is the prefix): the server marks the item current on first paint, and the runtime's active-link sweep keeps it lit across client navigations into the section — without it the exact-href sweep clears the highlight the moment the URL grows a deeper segment inside a kept shell. Both sides compare the bare path (/customers/and/customersare one section), andMatchPath: "/"lights only the root page, never every page.SuppressDrawerTrigger: trueplusui.SidebarDrawerTrigger(cfg)moves the hamburger into your own chrome (the page header, left of the brand): the standalone button is the same trigger — same class, samedata-cui-openwidget contract, same>= mdself-hiding from the component's stylesheet.MountSidebarstill mounts the drawer itself, once, as always.NativeMobile: trueadds a native disclosure for browsers with scripting disabled. CSS selects it with(scripting: none); scripted browsers keep the mounted drawer and its focus and Escape behavior. Keep callingMountSidebar. The fallback uses the same filtered items and group markup.
Filter toolbars: the URL-driven pattern
Set a distinct FilterToolbarConfig.ID for each toolbar on the same page.
That ID namespaces derived search, select, sort and pill IDs and their
labels and shortcut targets, without changing submitted field names.
Without an explicit toolbar ID, pass the page's shared Ctx ID scope to
deduplicate controls across renders (render.WithIDScope for standalone hosts).
ui.FilterToolbar is the control strip that sits above a DataTable or
card grid on a list screen. It renders one <form method="GET"> whose
controls carry the current filter/sort/search state. Submitting it (Apply)
navigates to <action>?facet=value&sort=…&q=…; the screen's Load(ctx)
reads those params and renders the filtered list server-side. Refresh,
share, and back-button all reduce to "same URL → same view" with no client
state. This is the "URL params are the source of truth" contract from
core-ui/ARCHITECTURE.md. Reset is a plain link back to the bare action, so
it clears every param with zero JavaScript. It works with the runtime
disabled. ClientNavigation: true opts submit into client navigation through
data-cui-spa; its successful controls become the destination URL query.
Use this when the filtered content belongs to the screen's replaced cell.
Leave it false when a kept layout also renders query-dependent content, so the
native GET reloads that layout too; Reset then carries data-cui-nav="off", so
it reloads the layout as well instead of letting the client router swap only
the screen cell. With ClientNavigation: true the router takes Reset too.
Client navigation honors the clicked submit button's name/value and form
overrides and asks the existing leave guard before discarding unsaved edits.
Facets render as a native <select> (default) or, per Kind: FacetPills,
a wrapping radio-pill group (short, glanceable choices). The toolbar is
responsive by construction: it declares itself a container and lays its
controls out with flex-wrap, degrading row → wrapped rows → single-column
stack as its own width shrinks (correct even inside a slim sidebar on a
wide screen). Every control, including Apply/Reset, stays on-screen and
tappable; nothing overflows a narrow ancestor, and pill labels never wrap
mid-label ("Waiting On Customer" stays one line).
// Screen.Render: the toolbar reflects the current URL state. func (s *CustomersScreen) Render() render.HTML { return ui.Stack(ui.StackConfig{Gap: ui.GapLG}, ui.FilterToolbar(ui.FilterToolbarConfig{ Action: "/customers", // the list route (the form GETs here) Facets: []ui.Facet{ {Name: "status", Label: "Status", Value: s.status, Options: []ui.FacetOption{ {Label: "Open", Value: "open"}, {Label: "Closed", Value: "closed"}, }}, {Name: "plan", Label: "Plan", Kind: ui.FacetPills, Value: s.plan, Options: []ui.FacetOption{ {Label: "Free", Value: "free"}, {Label: "Pro", Value: "pro"}, }}, }, Search: &ui.FilterSearch{Name: "q", Value: s.query, Placeholder: "Search customers…"}, Sort: []ui.SortOption{{Label: "Newest", Value: "created_desc"}, {Label: "Name A–Z", Value: "name_asc"}}, SortValue: s.sort, }), ui.DataTable(/* … rows filtered per s.status / s.plan / s.query / s.sort … */), ) } // Screen.Load: read the URL params the toolbar submits. func (s *CustomersScreen) Load(ctx context.Context) error { q := app.QueryFromContext(ctx) s.status, s.plan = q.Get("status"), q.Get("plan") s.query, s.sort = q.Get("q"), q.Get("sort") return nil // fetch + filter rows from s.* here }
An empty facet value (the auto-prepended "All" choice) submits status=;
the server treats an empty param as "no filter". Pair with FilterChipBar
below the toolbar to show the active filters as removable chips.
Dropdown: true is the compact layout a dense list wants: the row is
the search field (--ui-filter-toolbar-search-width, 22.5rem), then a Filters button, then the
Tools. The facets, the sort, the Extra controls and the one
Apply/Reset pair move into the button's ui.Dropdown panel, and its
badge counts the facets set plus Applied (the filters an Extra
control holds, such as a typed filter expression). Everything stays in
the one form, so Enter in the search submits the panel's fields whether
the panel is open or not. With nothing for the panel there is no Filters
button and no Apply; the search submits on Enter. Tools are links and
link menus (a Columns menu, a layout switch) drawn at the row's end;
they render inside the form, so a tool holding a <form> panics.
--ui-filter-toolbar-panel-width sets the panel's minimum width.
Set Compact: true with Dropdown: true to keep search, the filter trigger,
and tools on one row, including on phones. Search takes the remaining width.
SearchFieldsintegration. When the entity declaresSearchFields,FilterSearch{Name: "q"}maps 1:1 onto the list endpoint: the?q=param the toolbar emits is exactly what the auto-CRUD List handler free-text-searches. No manual wiring needed: the server-side search is automatic. WithoutSearchFields, the?q=param is ignored by the CRUD layer and the screen must filter rows itself (the pre-existing "wired manually" behaviour).
CodeBlock: line highlighting, diffs, wrapping
ui.Markdown forwards every fence option it recognizes onto
CodeBlockConfig, so a documentation fence can highlight lines, mark a
diff, wrap long lines, and cap its height without leaving Markdown:
| Fence option | Maps to | Effect |
|---|---|---|
title="main.go" / title=main.go (filename= too) | Filename | chrome header with the filename |
showLineNumbers (showLineNumbers=false off) | LineNumbers | left line-number gutter |
scroll | Scroll | caps body height at --ui-code-block-scroll-max, scrolls |
{1,3-5} or highlight=1,3-5 | HighlightLines []LineRange | background band on those lines (1-based, inclusive) |
diff | Diff | lines starting + (and +++) get an added band, - (and ---) a removed band; the marker stays in the text |
words="err,nil" / words=err,nil | HighlightWords []string | literal matches wrapped in <mark class="fui-code-block__mark"> |
wrap (nowrap / wrap=false off) | Wrap | soft-wrap long lines instead of horizontal scrolling |
Unknown options are ignored, and so is an invalid highlight= or
words= spec: a fence carrying options for another tool degrades to a
plain block rather than a broken one. The raw info string stays
addressable on the rendered block's data-meta attribute. A fence whose
language is diff gets no diff marking; only the diff option does,
because a highlighter may legitimately know the diff grammar.
All of these are also plain CodeBlockConfig fields for direct callers:
ParseLineRanges("1,3-5") builds the HighlightLines value, and the
same features work on the Lines (pre-highlighted) path, where diff
classification skips a leading token span and word marks stay inside
text nodes so they never split a caller's markup.
This exact fence renders live on the docs site:
func total(items []Item) int { sum := 0 for _, it := range items { sum += it.Price } return sum }
And the diff form, header lines and all:
--- a/api.go +++ b/api.go @@ handler @@ func get(w, r) { - user := load(r) + user := mustAuth(r) render(w, user) }
Line bands derive from the theme's status colours
(--color-primary for highlight, --color-success/--color-danger for
added/removed, --color-warning for word marks) and are tunable through
--ui-code-block-highlight-bg, --ui-code-block-added-bg,
--ui-code-block-removed-bg, and --ui-code-block-mark-bg.
A word mark keeps its token's color, so its tint stays at 12% (every
token color holds 4.5:1 on it) and a --color-warning underline carries
the emphasis.
Syntax highlighting
ui.HighlightLines(code, lang) is the kit's highlighter: a small
single-pass scanner that returns one render.HTML per source line, ready
for CodeBlockConfig.Lines. ui.Markdown fences and ui.CodeTabs
samples go through it. It emits the kit's token classes (tk-kw,
tk-fn, tk-str, tk-num, tk-com, tk-type, tk-pn), which the
CodeBlock sheet colors from the theme's Code slots (--tk-*, dark
values in Theme.DarkCode; see theming).
| Language key | What it classes |
|---|---|
go, golang | keywords and builtins, predeclared types, call sites, and named types in type position: after type, an exported composite literal (Pong{), behind * or [], package-qualified or not (*http.Request) |
js, ts, jsx, tsx, javascript, typescript | keywords, a few built-in types, call sites, object keys ({ method: …}, not a ternary's a ? b : c) |
sql, postgres, sqlite | keywords and column types, case-insensitive; -- and /* */ comments |
sh, bash, shell, console | keywords, the command word of each line or pipeline stage (after an optional $ prompt and its space), # comments at a word boundary |
yaml, yml | mapping keys (at line start, after an optional - list marker), true/false/null, # comments; an apostrophe in an unquoted value opens no string |
json | keys (tk-fn) apart from string values (tk-str), true/false/null |
| any other non-empty key | comments, strings and numbers |
"", text, plain | escaped text, no tokens |
Hand-tokenized lines work too: pass <span class="tk-…"> spans you
escaped yourself as Lines, and they draw in the same palette.
Go source is tab-indented, so the block renders a tab four columns wide
(--ui-code-block-tab-size, default 4; the browser's 8 pushed nested
code off a hero-width panel). The line-number gutter draws in a mix of
the code text and code surface colors (--ui-code-block-gutter-color),
so it recedes on a dark or a light code surface alike.
Common mistake: writing the line numbers as highlight={2}. The
braces are the bare form; highlight= takes the bare list. Both
{2,5-7} and highlight=2,5-7 work, {2,5-7} after highlight= does
not.
Adding a new component: checklist
The framework's drift tests catch most of these; this list is a helpful pre-flight read for human reviewers.
- Implementation:
framework/ui/<name>.go. - Theme-token CSS only: register your own
RegisterStyle; usevar(--color-*, fallback)etc. Never style it from an app: the docs site ships no stylesheet of its own and composes the kit as is. - Unit tests:
<name>_test.goexercising panic paths + emitted markup + variant classes. /components/<slug>screen inexamples/site/: register inmain.go, add an entry tocomponentCatalogincomponents.go. The catalog drives the site's page-level test loops (axe, single-<main>, …), so an unregistered page is an untested page.- Chromedp e2e in
examples/site/e2e_new_components_test.goore2e_new_components_interactions_test.go, ARIA shape for static components, real interaction (click / type / drag) for runtime-driven ones. core-ui/ARCHITECTURE.md: any newdata-cui-*attribute the runtime reads must land in the table here OR in the drift-test whitelist (with a justification comment). TheTestRuntimeAttrsAreDocumentedgate incore-ui/runtime/internal/treegate/attrdoc_test.goenforces it.- Axe:
TestAxe_AllPagesAreCleanruns axe-core against every catalog page and fails on any violation. The most common authoring mistakes it catches: missing tap target floor (44×44), role/aria-allowed-rolemismatches, color-contrast on tinted backgrounds, scrollable regions withouttabindex="0". - Composition first: before writing a new runtime module, see if
preset.Modal/preset.Popover/preset.Drawer+data-cui-open+data-cui-deeplink+ signal-binding already covers the case. Lightbox and NotificationBell each ship without a runtime module by composing existing primitives. - Behaviour registers like style: when a module is warranted, it
lives in your package, embedded beside the Go, and registers with
its markers:
registry.RegisterBehavior("<name>", js, registry.Markers("[data-<prefix>-x]")). It may alsoregistry.Requires("action")(or any embedded module or registered behaviour): the loader has the requirement registered before the module's script runs, on every load path. The host serves it at/__gofastr/runtime/<name>.js, the kernel scans the markers, and the module loads once when one appears. Keep the module contract (window.__gofastr.loadedModules[name] = trueon attach, a scanner underwindow.__gofastr._moduleScanners[name]), bind by attribute only, and use your owndata-prefix: adata-cui-*marker is admitted only when the attribute is already documented. The module is a runtime module and the runtime's source lints hold it:constandlet, nevervar; no selector or storage key built from a raw value; rest parameters and spread, neverarguments,.push.applyorArray.prototype.<m>.call; a class body, never an assignment to<Ctor>.prototype(core-ui/check, found through your//go:embed). Seecore-ui/ARCHITECTURE.md"Component behaviour".
Icons
framework/ui/icons exposes 291 vendored drawings as string constants: all 287
Feather icons plus Lucide's chevrons-up-down, receipt, flame, and diamond.
ui.Icon renders them inline with no JavaScript or image request. Each drawing
uses a 24 by 24 viewBox, a two-unit stroke, and rounded caps and joins. The
stroke inherits the surrounding text color. The live /components/icon page
shows each drawing as a centered 24px icon above its name. Click anywhere on
a tile to copy the name. The copied confirmation appears in that tile.
Its sticky header is a single row with search, a Filters dropdown for categories,
and an icon submit button. The result count scrolls above it. Each
drawing has one of 19 categories. Submit Search to combine the controls; Clear
removes both. Search matches words in names and aliases, or category labels,
ignoring case and treating hyphens and underscores as spaces. The q and
category URL parameters survive refresh, sharing, and browser history.
ui.CopyButton with Tile: true draws this full-width layout, with its Icon
above Label. It retains the usual Target element ID, accessible name,
keyboard handling, and announced confirmation. Tile icons render at 24px;
the native title exposes the full label when it is visually truncated.
--ui-copy-btn-tile-min-height controls the tile's minimum height, defaulting to 6rem.
Tile cannot be combined with IconOnly, Inline, or Ghost; those
combinations panic. Supply the target text in a separate element, as with any
other CopyButton.
package example import ( "github.com/DonaldMurillo/gofastr/core/render" "github.com/DonaldMurillo/gofastr/framework/ui" "github.com/DonaldMurillo/gofastr/framework/ui/icons" ) func SearchGlyph() render.HTML { return ui.Icon(icons.Search, ui.IconConfig{Size: "20"}) }
icons.Categories() returns a caller-owned slice of Category{ID, Label}
values in display order. IDs are the filter values. icons.CategoryFor(name)
returns (Category, bool), resolves aliases, and returns false for unknown
names. Category assignments live in categories.go, independently of the
unmodified upstream artwork.
To embed the same browser, call
gallery.RenderIconCatalog(gallery.IconCatalogConfig{Action: "/icons", Search: q, Category: category})
from framework/gallery. Action is the required GET route. Read q and
category from that route's request on each render. Empty or unknown category
IDs select all categories. An empty result shows a message with the controls
still available. The default gallery demo uses /components/icon.
icons.Names() lists every canonical name. The Go constants use PascalCase,
such as icons.ArrowLeft, icons.Search, and icons.Receipt. Alias constants
work the same way, including icons.Card and icons.PanelLeft. The gallery
searches aliases and shows canonical names on the tiles. Existing string names remain valid.
The aliases are alert-octagon to danger, alert-triangle to warning,
card to credit-card, chart to bar-chart, edit-2 to edit, fire to
flame, more to more-horizontal, panel-left to sidebar, pencil to
edit, sliders to settings, success to check, trash-2 to trash, and
x to close. These are catalog aliases. An application override through
ui.RegisterIcon applies only to the exact registered name, so overriding a
canonical name does not change its aliases.
| API | Behavior |
|---|---|
ui.Icon(name, cfg) | Renders one SVG. Unknown names return empty markup. |
ui.IconRegistered(name) | Reports whether a built-in, alias, or app registration exists. |
ui.RegisterIcon(name, body) | Adds or replaces an exact name with trusted inner SVG markup. Safe for concurrent use. Never pass user-supplied SVG. |
icons.Names() | Returns a caller-owned, sorted slice of canonical names; excludes aliases and app registrations. |
icons.Aliases() | Returns a caller-owned map of alternate names to canonical names. Alias targets are always canonical drawings. |
icons.Lookup(name) | Returns trusted built-in inner SVG markup and a boolean. Includes aliases; ignores app registrations. Custom renderers must supply the shared wrapper attributes. |
ui.IconConfig field | Default | Behavior |
|---|---|---|
Size | "20" | Width and height, as a numeric value or CSS length such as "1.25rem". |
AriaLabel | empty | Empty makes the SVG decorative with aria-hidden="true". A label sets role="img" and aria-label. |
ID | empty | ID on the SVG root. |
Class | empty | Additional classes beside fui-icon. |
ExtraAttrs | nil | Extra root attributes. Owned drawing and accessibility attributes, class, id, style, and data-cui-* are filtered. |
ui.LinkButtonConfig.Icon accepts the same names and renders the icon before
the visible label at 18 pixels. Leave that icon decorative. A standalone
meaningful icon needs AriaLabel; an icon-only control needs its accessible
name on the control itself.
App registrations take precedence over built-ins for the exact name. Overriding
check does not replace success. Registrations contain inner geometry only,
without an outer <svg>. The shared renderer supplies the wrapper.
The SVG files are unmodified copies from
Feather commit 3dc050d
and Lucide commit 7faf876.
The initial 20 Feather files were retrieved October 24, 2026 at 22:26:54 UTC;
the remaining files were retrieved October 25, 2026 at 05:59:29 UTC. Feather's
MIT license and Lucide's ISC license with its retained MIT notice are copied
verbatim into LICENSE.feather and LICENSE.lucide in the icon package, the
generated Go header, and THIRD_PARTY_NOTICES.md. Include the relevant upstream
notices when redistributing bundled artwork or generated drawing data.
framework/ui/icons/sources.json records the upstream commit, original file
path, pinned download URL, local path, SHA-256, and modifications for every SVG,
plus each file's source ID and retrieval timestamp. Each source has its own
repository, commit, retrieval time and license record. Aliases map to canonical
names without duplicating drawing files. Local names map to upstream
names: close uses x, danger uses alert-octagon, edit uses edit-2,
settings uses sliders, trash uses trash-2, and warning uses
alert-triangle. The upstream edit, settings, and trash variants are
available as edit-square, settings-gear, and trash-simple, preserving
the original 20 local names. Coordinates are unchanged. Generation extracts inner geometry
and normalizes XML serialization for the shared SVG wrapper.
To update the set, vendor files from a full upstream commit SHA and update the
manifest, license, and source documentation. Run go generate ./framework/ui/icons
from the repository root. Generation is offline and verifies the recorded
checksums and provenance before writing Go data. It rejects unsupported markup,
scripts, links, styles, inconsistent drawing attributes, and Go identifier
collisions with another icon or a handwritten declaration. Commit the sources,
manifest, notices, and icons_gen.go together. Do not redraw vendored SVGs;
register custom artwork through ui.RegisterIcon with its own provenance.
The site's header/search/GitHub glyphs and shared framework outline helpers render through the catalog. A source-level test checks literal icon references in the site, embedded app specs, gallery, and framework components. It also refuses private site registrations and inline site SVGs. Charts, app artwork, and filled rating shapes are separate component drawing code.
Common mistake: assuming any icon name exists. Check icons.Names() or
ui.IconRegistered before accepting a dynamic name. A typo renders no icon.
Common mistakes
- Writing a new runtime module when composition already covers it.
Check
preset.Modal/preset.Popover/preset.Drawer+data-cui-open+data-cui-deeplink+ signal binding first. NotificationBell ships with zero new JS by composing them; the Lightbox composed the same primitives and then earned a module for the behaviour composition cannot give (gallery stepping, pinch-zoom). New modules are the expensive path (budget, tests, docs). - Styling a component from an app. The docs site
(
examples/site) ships no stylesheet beyond its owned header, footer and docs-page sheets, and those never reach into a kit component. A component owns its CSS viaregistry.RegisterStylewith theme tokens, so it works in every host, not just the demo site. - Hardcoding colors/spacing instead of theme tokens. Use
{colors.*}/{spacing.*}/var(--color-*, fallback)so themed hosts and dark mode don't break your component. - Skipping the demo page + e2e pairing. Every
/components/<slug>page ships with at least one chromedp test and a package unit test (suite convention), andTestAxe_AllPagesAreCleanautomatically fails the build on any axe-core violation for every catalog page. Register the page and you've signed up for all three. - Adding a
data-cui-*attribute without documenting it. The runtime contract lives incore-ui/ARCHITECTURE.md; every attribute the runtime reads must be in its table (or an explicitly justified whitelist) before the change lands.
See also
docs/widgets.md: widget framework (mount, deeplink, signal lifecycle).docs/ui-getting-started.md: first-time setup for the UI layer.- runtime-contract: the SSR/hydration/island/SSE model +
data-cui-*attribute reference (embedded extract ofcore-ui/ARCHITECTURE.md). framework/ARCHITECTURE.md: package layout + extraction rules.ROADMAP.md§2: deferred UI components.