Mixins / foldkit-mixins-ui

Stateless vocabulary

These views own no Model, no Message, and no bundle. Call X.view(options, h). Every one takes an optional style of its slots and mixins beside it. Style the slots with the matching recipe. Hide text with U.srOnly from foldkit-mixins/utilities. Name a control with FieldAssociation; Label.view takes the id that derived.

ts
import { Style } from 'foldkit-mixins'
import { AspectRatio, AspectRatioSlots, Icon, IconSlots, Recipes } from 'foldkit-mixins-ui'

const Frame = Style.forSlots(AspectRatioSlots)(Recipes.AspectRatio({ ratio: 'video' }))
const Mark = Style.forSlots(IconSlots)(Recipes.Icon({ size: 'sm' }))

// `h` is the view's builder. `content` is `Html`.
AspectRatio.view({ content: h.span([], ['poster']), style: Frame }, h)
Icon.view({ content: h.span([], ['!']), label: 'Warning', style: Mark }, h)
Icon.view({ content: h.span([], ['!']), style: Mark }, h)

A label makes the icon role="img". Without one the glyph is aria-hidden. Icons.glyph is the other tool: a mask on an element that already exists.

ViewCallRecipe
Accordion{ id, items: { id, title, content, disabled? }[], open, onToggle, headingLevel? }; Accordion.toggled(open, id, { multiple?, collapsible? }) is the transition update applies, the open ids being the application's. Each trigger is a button in a heading naming its panel; a shut panel stays drawn and inert. The panel opens on a 0fr-1fr grid row, the chevron turns, and the open title turns semibold in its own width.variant: divided (default), plain. Slots root, item, heading, trigger, label, chevron, panel, body, content.
Alert{ title, description?, icon?, assertive? }. assertive is role="alert"; otherwise role="status".tone: info (default), success, warning, error. Slots root, icon, title, description.
AspectRatio{ content? }. Slot frame.ratio: square (default), video (16/9), portrait (3/4). The corner reads radius.
Avatar{ name, src }. Initials show until the image covers them.size: sm, md (default), lg. Slots root, fallback, image.
Badge{ text, tone? } writes data-tone.Recipes.Badge({ attribute, tones }) maps that attribute; values outside the map keep the base. Slot badge.
BannerA full-width notice across a page or a section: Banner.view({ label, message, icon?, action?, onDismiss?, dismissLabel?, style? }, h). The root is a section named by label, a region landmark rather than a live region, since a banner is there on load. action is { label, href } (a link) or { label, onClick } (a button); onDismiss draws a close button named Dismiss <label>. Whether it shows is the application's.tone: info (default), success, warning, error; emphasis: subtle (default, Alert's tint) or bold. Slots root, icon, message, action, dismiss.
Breadcrumb{ steps: { label, href?, current? }[] }. current is text with aria-current="page"; the others are links.No axis. Slots root, list, item, link, current.
ButtonGroup{ label, items }. role="group". The buttons stay the caller's. Segmented is the control that selects.No axis. Slot root.
Card{ title?, description?, content?, image?, media?, action?, footer?, press?, label?, selected?, disabled?, dismiss? }. press is { onClick } or { href, external? }: one button or link stretched over the card, named by label or the title (a pressable card with neither throws); action, footer and the dismiss button stay pressable above it. selected is aria-pressed on the button, aria-current on the link. dismiss: { onDismiss, label?, always? } is revealed on hover and focus unless always, and always on a touch screen. The root carries data-selected, data-disabled, data-pressable, data-dismissible (the header then keeps clear of the button).orientation: stacked (default), inline (image or media leading, footer trailing). surface: outlined (default), elevated, filled, plain. padding: compact, comfortable (default), roomy, flush. Slots root, image, media, header, title, description, action, content, footer, overlay, dismiss.
Card.group{ cards: (CardView & { key })[], columns?, fluidHover?, label?, cardStyle? }. A keyed list of cards in columns (default 1); each item carries data-row and data-column. With fluidHover (default) one highlight follows the pointer to the nearest card through FluidHover from foldkit-primitives/dom, reading both directions when there is more than one column, and a press in a gap presses the highlighted card. cardStyle styles every card that names no style of its own.Recipes.CardGroup: layout: divided (default, hairlines between rows and columns), separated. border: none (default), outlined. Use cards of surface: 'plain' inside. Slots root, list, item, highlight.
CarouselThe APG carousel, never rotating by itself: Carousel.view({ id, label, slides, index, onIndex, loop?, picker?, slideLabel?, previousLabel?, nextLabel?, pickerLabel?, style? }, h). A group with aria-roledescription="carousel", each slide a group named "2 of 5"; previous and next (aria-disabled at the ends unless loop), a dot per slide, and a polite status naming the slide shown. The track is a CSS scroll-snap scroller, so swipes move it natively. The application keeps index and answers onIndex with the Carousel.ShowSlide({ id, index }) Command, which scrolls the slide into place once the render commits. A swipe is not reported back, so index stays where the last Message left it.peek: none (default) or edge. Slots root, track, slide, controls, previous, next, picker, dot, status.
CheckboxGroup{ id, legend, options: { value, label, description?, disabled? }[], checked, onToggle }; CheckboxGroup.toggled(checked, value) is the transition. A fieldset named by its legend, each option the APG's checkbox (role="checkbox") named by a label for it, so a press on the words toggles it.variant: plain (default), list (checked rows fill, and merge where they stand together). The checkbox and label are the Checkbox recipe's pieces. Slots root, legend, option, checkbox, label, description.
CodeBlock{ code, tokens?, language?, title?, lineNumbers?, copy? }. tokens are { from, to, kind }, as foldkit-richtext-code and foldkit-richtext-code-shiki tokenizers return them; each is drawn with data-kind, and a token out of order or outside the code throws. The block's text is the code exactly. copy: { onCopy?, copied? }: onCopy gets the code, so run copyText from foldkit-primitives; without it the button has no handler, for a page whose script copies. CodeBlock.linesOf is the cut.No axis. Colors each kind from the theme's syntax group; data-line-numbers numbers lines, data-copied marks a copy done. Slots root, header, title, copy, body, code, line, token.
ColorPickerColorPicker.view({ id, label, value, onChange, presets?, presetsLabel?, channelLabels?, disabled?, style? }, h): an OKLCH color (ColorPicker.Oklch, { l, c, h }) as a group of three labelled native ranges, each saying its value with its unit in aria-valuetext, a swatch, the color as text, and presets as a radiogroup of native radios. format/parse (an Option) turn it to and from CSS text; withChannel sets one channel within its range.size: sm, md (default), lg. Tracks paint gradients from the root's --fk-color-l/-c/-h by [data-channel]. Slots root, label, swatch, value, channel, channelLabel, slider, presets, preset.
Command{ id, label, placeholder?, groups: { heading?, items: { value, label, keywords?, shortcut?, disabled? }[] }[], query, onQuery, active, onActive, onRun, empty? }; Command.filtered(groups, query) is what a query leaves. shadcn's Command and Fluid's CommandMenu: the field points at the highlighted row through aria-activedescendant, ↑ and ↓ wrap through enabled rows, Enter runs the highlighted one; rows never re-sort, and a filtered-away highlight moves to the first enabled row.No axis. Slots root, input, list, group, heading, item, shortcut, empty.
ContextMenuContextMenu.target({ onPoint, onOpen }, h) on the area, and ContextMenu.toView(mixins, { h }, point) as @foldkit/ui's Menu toView; update keeps the point and folds Menu.Message.Opened on onOpen. The Menu opened where a right press landed (or, from Shift+F10 or the ContextMenu key, at the area), its keys, typeahead and rows the Menu's.The Menu recipe.
DateRangeA start and an end day on the Calendar. The application owns the range: fold the Calendar's SelectedDate through DateRange.picked(range, date) (start, then end, swapped if earlier, then a new start), and draw with DateRange.toView(range, mixins, { h }) and maybeSelectedDate: Option.none(), which marks each day cell and every chosen day aria-selected.size (the Calendar's); Recipes.DateRange is Recipes.Calendar.extend, filling the ends and drawing the band. Slots CalendarSlots; dayCell marked data-range-start, data-range-end, data-in-range.
Empty{ title, description?, icon?, action? }.No axis. Slots root, icon, title, description, action.
HoverCard (recipe)Styles @foldkit/ui's HoverIntent (HoverIntentSlots): a trigger read as a link, a raised panel fading in on the fast tier as it is drawn. Place the panel with the Anchor Mount; the open and close delays are HoverIntent's (200ms and 300ms by default).No axis. Slots trigger, panel.
Icon{ content, label? }. A label is role="img"; without one the glyph is aria-hidden.size: sm, md, lg on the type scale; none (the default) stands on the size ladder, 16px or 14px compact. Slot glyph.
InputCopyA read-only value with a copy button, Zag's clipboard: InputCopy.view({ id, label, value, copied, onCopy, copyLabel?, copiedLabel?, style? }, h). The view is stateless: answer onCopy with the InputCopy.Copy({ text }) Command, which yields foldkit-primitives/dom's ClipboardMessage (Copied or CopyFailed, never a throw); the application sets and clears copied. The button is named Copy <label>; copied, it reads Copied and a polite status region says so.No axis. Slots root, label, group, input, button, status.
Item{ content?, media?, title?, description?, actions? }; content follows the title and description.density: comfortable (default), compact. Slots root, media, content, title, description, actions.
Kbd{ key }. Names the key.No axis. Slot key.
Label{ for, text }. for is the control's id.No axis. Slot label.
MenubarA row of @foldkit/ui Menus, shadcn's and the APG's menubar: Menubar.view({ label, model, menus, onMessage, style?, menuMixins }, h), its Model (Menubar.init(ids)) folded with Menubar.update, whose Selected OutMessage names the menu and the row. One trigger is the tab stop; ArrowLeft/ArrowRight (Home/End while all are shut) move it, and with a menu open they open the next on its first row. While one is open, pointing at another opens it, and opening any shuts the rest. Recipes.MenubarMenu is the Menu recipe with a bar's flat triggers.No axis. Slots bar, menu; each menu's are MenuSlots.
Meter@foldkit/ui's Meter: Meter.view({ id, label, value, min?, max?, low?, high?, optimum?, valueText?, style? }, h) draws a meter named by its label. The root says how good the value is as data-level (good, fair, poor), as the HTML meter judges it; Meter.levelOf is that judgment. A measurement, where Progress is work.No axis; the fill takes success, warning or error by level. Slots root, label, track, fill.
Nav@foldkit/ui's Nav adapted: Nav.toView(mixins, { h }, label, draw?) as the Nav's toView, drawing nav > ul > li > a with the current link aria-current="page"; Nav.fluid beside the style glides a line to the current link and a faint pill to the one the pointer nears. shadcn's navigation menu without its dropdown panels; the keys are the browser's.No axis. Slots nav, list, item, link, indicator, highlight.
NestedMenuA menu whose rows open submenus beside them, one level deep: NestedMenu.init(id, rows) (a row with items hosts a submenu), folded with NestedMenu.update, drawn with NestedMenu.view({ model, rows, buttonContent, onMessage, style?, menuMixins, submenuMixins }, h). A submenu's row is a menuitem with aria-haspopup="menu" and aria-expanded; ArrowRight, Enter or Space opens it on its first row, ArrowLeft or Escape closes it back to the row, a pointer resting 150ms opens it, and choosing a leaf closes everything. Each panel is @foldkit/ui's Menu; Selected names the submenu a row came from.No axis. Slots sub, label, chevron; the panels are MenuSlots, styled by Recipes.Menu.
NumberField{ id, label, description?, value, onChange, min?, max?, step?, page?, format?, disabled? }; NumberField.settled(value, bounds) clamps a value onto its step. The APG's editable spinbutton: an input with role="spinbutton" that the arrows, PageUp and PageDown, Home and End step, a typed number committing on change, between steppers out of the tab order that disable at their bound.No axis. Slots root, label, group, input, decrement, increment, description.
OtpField{ label, value, onChange, length?, pattern?, groups?, disabled? }; OtpField.kept(text, length, pattern) filters a code. shadcn's input-otp: one real input (autocomplete="one-time-code", paste and Backspace the browser's) over cells that mirror it, the next cell marked data-next for its caret.No axis. Slots root, input, group, cell, separator.
Pagination{ page, pageCount, onPage, label? }. Fewer than two pages draws nothing. Pagination.itemsOf(page, pageCount) is the stops.No axis. The current stop reads aria-current. Slots root, prev, next, page, ellipsis.
PasswordInputPasswordInput.view({ id, label, value, onInput, visible, onToggle, autocomplete?, toggleLabel?, description?, invalid?, disabled? }, h): a password field whose toggle button (aria-pressed, aria-controls, one fixed name) shows it as text. autocomplete is current-password (default) or new-password; spellcheck and autocorrect stay off. The app owns the text and the flag.size (sm, md, lg). Input's pieces in InputGroup's box; the eye is Icons.glyph by aria-pressed. Slots root, label, group, input, toggle, description.
Progress@foldkit/ui's Progress: Progress.view({ id, label, value, min?, max?, valueText?, style? }, h) draws a progressbar named by its label. value is an Option; none is indeterminate. The root carries the share done as --fk-progress.shape: bar (default) or ring, Circular Progress. Slots root, label, track, indicator.
RangeSliderRangeSlider.view({ id, label, value: { low, high }, onChange, min?, max?, step?, format?, thumbLabels?, name?, disabled?, style? }, h): a group named by its label holding two native input type="range" thumbs over one track. Each thumb is bounded by the other (the low one's max is the high value), so the browser stops a drag or a key at the other thumb. RangeSlider.moved(range, 'low' | 'high', value, bounds) snaps, bounds and orders a range, and returns the same range when nothing changed.tone: accent (default) or neutral; size: sm, md (default), lg, the Slider's look. Slots root, label, control, track, range, thumb.
RatingRating.view({ id, label, value, onChange, max?, starLabel?, readOnly?, disabled?, style? }, h): a radiogroup of native radios named by its label, one per star under one name, so the browser's arrows move the choice and a form submits it. value is an Option; none is unrated. Read-only, it is one img whose name says the rating.tone: warning (default, gold) or accent; size: sm, md, lg. Stars are a CSS clip-path on each radio's ::before; pointing previews the rating. Slots root, label, star.
ScrollArea{ content }. The height that makes it scroll is the caller's mixin. The viewport is tabbable.orientation: vertical (default), horizontal, both. Slot viewport.
ResizableResizable.Model (init(share)), its Message and update(model, message, bounds), which the application keeps and folds; Resizable.view({ model, onMessage, label, first, second, orientation?, min?, max?, step? }). shadcn's Resizable and the APG's window splitter: the handle is a separator with its value, stepped by the arrows and sent to the bounds by Home and End; a drag follows the Move Mount from where it began over the group the Resize Mount measured.No axis; data-orientation lays it out. Slots root, first, handle, second.
Separator{ orientation? }, horizontal by default. A vertical rule needs its container to set the cross size.orientation: horizontal (default), vertical. Slot rule.
Sheet / Drawer (recipes)Style @foldkit/ui's Dialog (DialogSlots), drawn as a Dialog is: the panel pinned to an edge of the viewport, sliding in from it on the slow tier as it fades (reduced motion keeps only the fade), bordered on the side facing the page. Recipes.Drawer is the Sheet from the bottom, its inner corners rounded and a handle drawn on the panel as a pseudo-element. The focus trap, Escape and backdrop dismissal stay the Dialog's.side: top, right (Sheet's default), bottom (Drawer's default), left. Slots are DialogSlots.
Sidebar{ id, label, sections: { id, title, links: { label, href, current? }[] }[], open, onToggleSection, collapsed, onToggleCollapsed }; Sidebar.toggled(open, id) flips a section. shadcn's sidebar with Fluid's collapse: a named navigation landmark of sections that open like an accordion's (several at once), a toggle that collapses the whole column off the page (no icon rail), the current page aria-current. Shut sections and a collapsed column stay drawn and inert.No axis. Slots root, toggle, nav, section, trigger, chevron, links, item, link.
Skeleton{ label? }. role="status". Stack one bar per line.shape: bar (default), circle. Slot bar.
Spinner{ label? }. role="status".size: sm, md (default). Slot wheel.
StepsSteps.view({ label, steps: { title, description? }[], current, onSelect?, style? }, h): a stepper, an ordered list named by label. The step at current is aria-current="step", those before it data-state="complete" (a check replaces the number), those after upcoming. With onSelect(index), completed steps are buttons for going back; steps ahead never are. The application owns current.orientation: horizontal (default) or vertical. Slots root, item, step, trigger, marker, text, title, description, connector.
Table{ columns: { header, value, numeric? }[], rows, caption?, fluid? }. With fluid, one highlight behind the body rows follows the pointer, and the lines beside the lit row go. Sorting and selection stay with foldkit-data-grid.No axis. Slots frame, highlight (with fluid), root, caption, head, headerRow, headerCell, body, row, cell.
TagsInputZag's tags input, a small Model of its own: TagsInput.init({ id, tags?, delimiter? }), folded with TagsInput.update (which returns a focus Command), drawn with TagsInput.view({ model, onMessage, label, placeholder?, disabled? }, h). The tags are a list of chips named by the label, each with a Remove <tag> button, then a text input. Enter or the delimiter (default ,) adds the draft split at the delimiter and trimmed, refusing empty and repeated parts; Backspace on an empty draft removes the last tag; a removal returns focus to the input. Keys reach update, which decides from its own draft.No axis; data-disabled on control. Slots root, label, control, list, tag, remove, input.
TreeViewThe APG's and Zag's tree view, a small Model of its own: TreeView.init({ id, open? }), folded with TreeView.update(nodes) (curried over the tree, flattened once), drawn with TreeView.view({ label, model, nodes, onMessage, style?, mixins? }, h) over nodes { id, label, children?, disabled? }. One tab stop; Up/Down move through the rows showing, stepping over shut and disabled ones; Right opens or steps in, Left shuts or steps out; Home/End; a character moves to the next showing row that starts with it; Enter, Space or a click chooses a row and opens or shuts a branch. Keys reach update, which decides from its own Model and sends a focus Command. Each item carries --fk-tree-level; a shut group stays drawn and inert.No axis; data-state on a branch item and its group. Slots root, item, row, chevron, label, group.
Typography{ level, text }. The level chooses the element (h1–h4, lead, body, small, muted, quote, code, and the interface roles display, title, subtitle, caption, micro as paragraphs).level, same set; a role is its size and leading on the size ladder. Defaults to body. Slot text.
VirtualList@foldkit/ui's virtual list: VirtualList.view({ model, items, itemToKey, itemToView, onMessage, overscan?, itemToRowHeightPx? | dynamicRowHeights?, style? }, h) draws only the rows in view. Its Model is upstream's (@foldkit/ui/virtualList's init), kept by the application and folded with that module's update. A row's mixins may style it, not handle its events, since it is drawn inside the list's submodel.No axis. Slots root (the box it scrolls within, 18rem tall by default) and row.