Skip to content

THE FIELD GUIDE

Component API

Control props, keyboard behavior, tables, overlays, forms and lifecycle contracts.

View source · ui/next/kit/README.md
On this page

The kit composes semantic elements, events and owned state from ui/next. Load theme.css and place controls inside a .vui ancestor. Semantic CSS variables supply colors and spacing; data-theme="dark" changes the palette. Recipes are scoped to their component classes and can be overridden by an application. Defaults live in the named vui cascade layer; normal unlayered application rules override them. See styling boundaries and layer order. The complete planned component library is still in development.

Button, Field, Switch and Badge retain native button/input/label semantics. Switch uses a checkbox with the switch role and supports ordinary label clicks and Space. Decorative artwork does not intercept its input. Required IDs are provided by the caller and must be unique in the document.

Checkbox(CheckboxProps) and Switch(SwitchProps) share a native checkbox input. Supply a Checked reader for controlled checkedness, or omit it and supply DefaultChecked for browser-owned state. OnChange receives the native boolean. The native default is also the form reset baseline. For controlled fields, reset keeps the owner's current value; handle the form's reset event to reset application state. Checked readers and a true DefaultChecked cannot be combined.

Checkbox also accepts Indeterminate func() bool for mixed presentation, useful on a master checkbox controlling several choices. Checked continues to describe native checkedness and form participation; mixed presentation does not submit a third value. Native click/Space clears mixed presentation and calls OnChange with the new checked value. Derive the mixed reader from the selected children and update them in OnChange. Omit the reader to release control. Switch rejects this reader because its role has two states. See native property and SSR semantics.

RadioGroup(RadioGroupProps) uses a fieldset, legend and a keyed list of native radio inputs. Value is an optional controlled reader; DefaultValue selects the initial/reset option when no reader is supplied. Option values must be unique and nonempty. An empty value leaves the group unselected. Name defaults to ID; use different names for independent groups in the same form. The browser owns arrow navigation, selection, grouping, disabled handling and required validity. OnChange reports only the newly checked enabled choice.

CheckboxGroup(CheckboxGroupProps) composes the native Checkbox inside one fieldset/legend. Supply a live Value func() []string; OnChange receives the new selection in option declaration order. Options and selected values are unique, and every selected value must identify an option. Empty option values are supported. Each input shares Name (default ID), so native FormData contains repeated names. The handler reads the latest selection on each event, including several edits and an immediate submit in one browser turn.

The group exposes Form, Disabled, Hint, Error, OnBlur, Class, InputClass and InputRef (the first enabled option), plus an Option content function. Native Tab and Space operate each checkbox; no custom arrow-key model is installed. Handle the form reset event to reset controlled application state. Disabled selections remain in the application model and are omitted from native FormData. Validate minimum/maximum selection counts in the form, using the group's InputRef to focus its first enabled choice on failure. For browser-owned individual choices, use Checkbox with DefaultChecked.

Select(SelectProps) uses the native select and option elements. Supply Options with unique values and nonempty labels. Value reads controlled selection; DefaultValue sets the native initial/reset selection when the reader is omitted. Without either, the browser selects the first available option. Placeholder adds a disabled empty option, useful with Required; it cannot be combined with an explicit empty-value option. Unavailable options retain their disabled state. OnChange accepts only an enabled listed value. It supports one selected value; multiselect is a separate capability.

MultiSelect(MultiSelectProps) uses a labelled native select with multiple selection. Supply Value func() []string for controlled state or DefaultValues for native initial/reset selection; these cannot be combined. OnChange receives a copied complete list in option order. Options and selected values must be unique and declared; empty option values are supported. At most 4096 options are allowed. Size defaults to four visible rows. Browser keyboard, touch, disabled-option and form behavior stay native. Selected disabled options remain in the event/model; native FormData omits them. Pair the component with form.All/ChangeAll, or use form.MultipleSelect to bind your own native select markup.

Checkbox, Switch, RadioGroup, Select and MultiSelect expose Name, Form, Disabled, Required, Hint, Error, OnBlur, InputRef, Class and InputClass. Form explicitly associates a control with another form; omitting it keeps the browser's ancestor association. Checkbox/Switch Value sets the submission value, defaulting to native on; unchecked controls are omitted from FormData. RadioGroup InputRef identifies the enabled controlled selection or enabled initial choice, falling back to the first enabled option. It supports invalid-field focus; browser-owned selection changes do not rebind that ref. Optional LabelContent (checkbox/switch) and Option (radio) supply noninteractive content while Label remains the accessible name. Theme part classes preserve native behavior, including switch direction in RTL.

Slider(SliderProps) supplies a horizontal native range input. Min/Max default to 0..100 when both are zero; Step defaults to 1. Bounds and values must be finite, with increasing bounds and a positive step. Supply an optional Value reader for controlled input, or a DefaultValue reader for browser-owned input/reset defaults. With neither, the browser chooses its native midpoint. The readers cannot be combined. Choose step-aligned values; native range sanitization owns snapping. OnChange receives continuous input and OnCommit receives native change events. Native controls supply pointer dragging, keyboard limits and direction handling. ValueText describes a unit or meaningful label; keep it synchronized with the current value. Names, form association, disabled state, descriptions, errors, blur, refs and styling use the same field conventions as the selection controls. Vertical and multiple-thumb sliders remain separate capabilities. See the HTML range contract and slider accessibility pattern.

Progress(ProgressProps) uses a labelled native progress element. Max defaults to 1; Value is finite and within 0..Max. Indeterminate removes its value attribute. ValueText can explain meaningful units; Class and BarClass expose styling. The component has no timer or hidden task state. It follows the HTML progress element.

Spinner(SpinnerProps) exposes a polite status containing Label (default Loading). Decorative hides it from assistive technology when another nearby status already describes the work. CSS supplies animation and respects reduced-motion settings. Alert(AlertProps, content...) groups a title and arbitrary content, with optional OnDismiss and DismissLabel. Variant accepts info, success, warning or danger. Its default status announcement is polite; Urgent opts into an alert independently from visual appearance. The owner removes the message and chooses an appropriate focus target after dismissal. Neither feedback component owns a timer.

Presence(PresenceProps, content...) retains ordinary-flow content during exit. Supply a unique ID and a live Visible reader; render Presence continuously and let it own the child's mounting lifetime. Its wrapper exposes data-state=open or closed, becomes inert and hidden from accessibility APIs as exit begins, and unmounts its children after the wrapper's current finite animations settle. Changing Visible back to true during exit cancels that wait and preserves the live subtree. Opening after completed exit creates fresh child state. Keep longer-lived data in the owner when it must survive complete dismissal.

The default theme fades opacity over 180ms. Class can replace the wrapper's CSS motion; put the exit transition/animation on that wrapper. Child animations and infinite animations do not extend its lifetime. TimeoutMilliseconds defaults to 5000 and accepts 1..60000; a timeout or host failure also disposes closing content. Cancellation releases observation without changing application-owned animation. There are no motion timers during ordinary visible updates. Native Dialog and Popover keep their separate presentation lifetimes; this component currently covers ordinary document flow.

Supply ReturnFocus for interactive content, pointing outside the closing subtree. Exit returns focus there only when focus was inside; arrival never moves focus. The default theme disables motion for prefers-reduced-motion: reduce; preserve that rule when overriding CSS. No-animation exits complete without a fixed delay. CSS entry uses starting styles and observation uses the Web Animations API.

kit.Presence(kit.PresenceProps{
    ID: "details", Visible: func() bool { return shown.Get() },
    ReturnFocus: trigger,
}, Details())

NewToasts(scope, key, ToastOptions) creates a notification queue in a component scope. Pass it to one descendant ToastRegion; an application-level owner can share it through context. Capacity initializes once, defaults to 32 and accepts 1..64 entries, including pending messages. Show returns false when full and keeps existing entries. Reusing a Key replaces that message in place and restarts its countdown. Close removes one key; Clear removes all. Disposing the owner releases the queue, handlers and pending work. Queues are independent across roots and SSR.

ToastMessage requires a Key and Title. Key is at most 128 bytes, Title at most 512 and Description at most 8192. Variant uses the feedback palette; Urgent selects an assertive announcement. ActionLabel and OnAction must be supplied together. Activating an action removes that message before calling its handler, so a burst cannot execute a removed message twice. Messages default to persistent; explicitly set DurationMilliseconds within 5000..86400000 for temporary information that is also available elsewhere. This follows the timing considerations in WCAG's toast example and the pause-on-interaction behavior described by React Aria.

ToastRegion requires ID, Queue and a ReturnFocus ref to a logical return target. Label and DismissLabel are localizable. Visible defaults to 3 and accepts 1..5; pending messages start neither timers nor announcements until visible. Each message has its own scope and countdown. Hovering the region, focusing any of its controls, or an inactive document pauses timers while retaining their remaining time. Replacing a message resets its countdown. The Now function is an optional, stable elapsed-millisecond clock for deterministic tests; keep it unchanged while the region is mounted.

Arriving messages preserve current focus. Queue.Focus lets an application provide a visible entry command or shortcut; the component does not install a global keyboard shortcut. Close buttons and actions are ordinary keyboard stops. Escape within a notification dismisses it. Removing a focused message moves focus to a remaining visible message, or ReturnFocus after the last one. Programmatic closure uses the same rule. Live announcement text is committed after its empty region, and excludes action/close labels. It supports semantic announcements; real screen reader verification remains part of product acceptance.

Mount the region near the application root for a viewport corner placement. Class and vui-toasts, vui-toast-body, vui-toast-actions, vui-toast-action and vui-toast-dismiss expose styling. A local region can instead belong to a page or preview so navigation disposes its messages. The default recipe wraps long text and scrolls when the visible stack exceeds the viewport.

notes := kit.NewToasts(scope, "notes", kit.ToastOptions{})
trigger := ui.Ref(scope, "notify")
send := kit.Button(kit.ButtonProps{OnPress: func() {
    notes.Show(kit.ToastMessage{Key: "saved", Title: "Your idea is saved"})
}}, ui.Text("Save a note")).Ref(trigger)
region := kit.ToastRegion(kit.ToastRegionProps{
    ID: "notifications", Queue: notes, ReturnFocus: trigger,
})

Dialog(DialogProps, content...) uses a native modal dialog with a visible title, optional short description, and a close button. Its controlled Open reader follows OnOpenChange; an optional PreventClose reader disables the standard close button and Escape request. Both read current state for each event, including input bursts. CloseLabel replaces the default close-button label and Class extends its recipe. Native form closure still synchronizes the owner. Supply ReturnFocus for a custom trigger or another logical return target; this also covers browsers which do not focus a button when it is clicked. Use autofocus or an ElementRef to choose an appropriate initial control. Closed dialog content stays mounted, preserving its state and effects; conditionally remove the component to dispose it.

Set Motion: true to opt into the shared Dialog/AlertDialog entry and exit presentation. The theme fades the panel and backdrop over 180ms; Class can override the CSS. The component exposes data-state="open" or "closed". Open becomes false immediately when dismissal is accepted. During exit the native dialog remains modal, the closing content is inert and hidden from accessibility, and background interaction/scroll stay blocked. The native layer closes and ReturnFocus is applied together after finite motion finishes.

MotionTimeoutMilliseconds defaults to 5000 and accepts 1..60000 when Motion is enabled. Timeout, animation cancellation and provider failure release the layer; there is no fixed delay when CSS has no animation or reduced motion is enabled. Reopening cancels the old wait, keeps the same content/state and focuses the dialog panel. Conditional removal cancels the wait and removes the layer immediately. Reload restores the owner's data and creates fresh presentation ownership. Native dialog.close() and native form dismissal have already closed the browser layer; their synchronization completes without retaining that layer.

Treat Motion as a fixed component configuration. Its implementation adds an owned component scope; use Class and other props to configure the dialog element. Keep state-driven changes to Open in the owner, and let this presentation return focus after exit. Low-level Modal and Popover retain their existing lifetimes.

AlertDialog(AlertDialogProps, content...) shares that modal presentation and focus handling. ID, Title, Description, Open, OnOpenChange, ConfirmLabel and OnConfirm are required. A cancel button receives initial autofocus; CancelLabel defaults to Cancel. The confirmation is a native button and never the default autofocus target. Class and ConfirmVariant customize appearance. Additional content can contain progress and a local error message. The alertdialog role, label and description follow the APG alert-dialog pattern.

The owner controls confirmation: synchronously close Open or set Busy before starting work. Optional Busy reads the live pending state and disables both decisions and Escape. Leave Open true and clear Busy on an unsuccessful operation to permit retry. Native closure still synchronizes the owner. ReturnFocus applies to dismissal and synchronous confirmation closure; when completing asynchronous work or closing programmatically, the owner can request its logical return target in the same handler when Motion is disabled. With Motion enabled, its presentation returns focus after release. Do not make important decisions depend on a timer.

Breadcrumb(BreadcrumbProps) renders a labelled navigation landmark containing an ordered list. Items have unique Key, Label and Href; the final current item may omit Href to render text. Ancestors require links. It accepts 1..64 items and defaults its navigation label to Breadcrumb. Optional Link renders an anchor, such as navigation.Link(item.Href, ui.Text(item.Label)), preserving router and native modified-click behavior. Class extends the recipe. The last item receives aria-current=page; CSS separators are decorative and wrap on narrow screens. Its semantic contract follows the APG breadcrumb pattern.

The lower-level Element("dialog").Modal(open) keeps presentation separate from ordinary attributes. It owns open; changing between managed and unmanaged presentation replaces that element. SSR leaves it closed until client activation. The host uses native modal layering and background inertness, applies final presentation after DOM writes, closes nested layers in reverse order, and shares a reference-counted scroll lock across roots in the same document. Removing or closing a root releases its dialogs. Explicit focus requests apply afterward. The host keeps Tab traversal within the modal, accounting for native disabled fields, radio groups and visible focus targets, including platforms whose default Tab order skips buttons. Other keys retain their native behavior. Native closure releases scroll ownership even without a guest close handler; closing an ancestor also closes nested modal descendants.

Tabs(TabsProps) is a controlled tab set with automatic keyboard activation. Value reads an enabled TabItem.Key; IDs and accessible labels are required. Keyboard navigation reads the current value on each event, including several keystrokes before the next DOM commit. One ref follows the active tab, so changing tab collections does not retain a ref for each historical key. Arrow keys wrap and skip disabled items, Home/End reach the ends, and vertical/RTL orientation changes the active keys. Tab and unrelated/composing keys retain their native behavior. Inactive panels stay mounted, keeping their state and effects. Use conditional component removal to release expensive panel resources when needed. Labels may scroll horizontally on narrow screens; explicit focus brings the target into view. The implementation and regressions follow the WAI-ARIA tabs pattern.

FormField(FieldProps, control) associates a label, hint and optional error with a native control. It composes caller classes and description IDs. Pair it with the optional forms package for validation and submission state.

Accordion(AccordionProps) exposes independently keyed sections, optional multiple expansion, and a Required mode that retains at least one open section. HeadingLevel defaults to 3. Native buttons supply Enter/Space activation and ordinary Tab order. Disabled items cannot toggle; the last required panel uses aria-disabled while its heading stays focusable. Panels remain mounted when collapsed, retaining state and effects. Conditional removal disposes them.

Expanded is a state reader, paired with OnChange. It reads the current value for every action, including multiple clicks in one input batch. A captured array snapshot would lose rapid toggles. For example:

expanded := ui.State(scope, "sections", func() any { return []string{"start"} })
sections := kit.Accordion(kit.AccordionProps{
    ID: "ideas", Items: items, Multiple: true,
    Expanded: func() []string { return expanded.Get().([]string) },
    OnChange: func(keys []string) {
        expanded.Update(func(any) any { return keys })
    },
})

The optional Title renderer receives key, label, expanded and disabled state and supplies noninteractive content inside the native button. Keep an accessible name. Class and the vui-accordion-* part classes expose styling without replacing behavior. Regions optionally adds named landmarks; enable it selectively for small sets or panels with nested headings. A caller-supplied TriggerRef supports focus management after programmatic closure. Ordinary activation returns focus to its heading in the same commit. The behavior follows the WAI-ARIA accordion pattern.

Popover(PopoverProps, content...) supplies a controlled native nonmodal dialog beside its trigger. Open is a live state reader, paired with OnOpenChange, so rapid activations read the current value. ID and Label are required; Title defaults to Label. Trigger optionally renders noninteractive content inside the button. TriggerClass, Class and the vui-popover-* parts expose styling; TriggerRef can identify a caller-owned focus target. Inputs may use autofocus for initial focus. Closing keeps the content mounted. Conditional removal releases its resources. Closed content is inert and hidden from the accessibility tree.

Motion: true opts into the theme's 160ms native CSS fade. Closing immediately ends native presentation and updates focus/interaction; the positioning binding survives until finite exit motion finishes. A quick reopen keeps the same content and cancels the old cleanup. Paused motion has a five-second cleanup deadline; root, anchor or node disposal releases the binding immediately. The theme uses display/overlay discrete transitions where supported and closes immediately elsewhere or with reduced motion. Put custom transitions on the popup element; child or infinite animations do not delay cleanup. See the native popover animation guidance.

Escape and outside-pointer dismissal use native auto-popover behavior. Native toggle notifications synchronize the controlled state; outside dismissal keeps the user's new focus target. The close button returns focus to the trigger. This nonmodal layer permits normal navigation outside its content. Its trigger becomes interactive after client activation, as with the controlled Dialog recipe.

The lower-level Element(...).Popover(PopoverOptions) requires an anchor ref in the same root. Placement accepts bottom-start (default), bottom-end, top-start, top-end, left and right. Start/end follow the anchor's direction. The host flips toward available room and shifts within the visible viewport, then tracks anchor, content, scroll and viewport changes. The kit constrains content using the supplied --ui-popover-width and --ui-popover-height CSS variables. Use --ui-popover-anchor-width to match the trigger's width. The binding owns position/left/top/right/bottom/margin while open and restores prior inline values when released. data-ui-popover-side exposes the resolved side for styling.

An absent anchor postpones opening. Removing or hiding a live anchor closes its layer; closing an ancestor Dialog also closes a popup inside that dialog. Native range movement preserves open presentation, focus and input selection. Validation rejects anchors within their own popup before changing the DOM. toggle events report the live open/closed state, including native details elements.

This implements the HTML popover contract. Arbitrary portals retain their separate ownership requirements.

Tooltip(TooltipProps, trigger) adds a noninteractive text description to one element trigger. ID and Text are required. It appends its description ID to the trigger's existing aria-describedby, retaining the trigger's events and ref. A small wrapper observes hover/focus without replacing the trigger's listeners. Use a native element or a recipe returning an element, such as kit.Button. Keep essential information available in visible text as well.

Keyboard focus opens immediately. Mouse and pen hover open after 300ms; leaving allows 150ms to cross into the hint. Hovering the hint keeps it visible, and the default CSS includes a pointer bridge across the positioning gap. Touch hover does not open it. Escape or activation closes it until hover/focus leaves. Timers belong to the component and are cancelled on changed intent or removal. Optional Delay: &TooltipDelay{Open: ..., Close: ...} overrides both durations, including zero; supported values are 0..60000ms. Placement uses the shared popover positions. Disabled prevents presentation. Class and TriggerClass customize parts.

Tooltip uses native popover="hint", which coexists with its containing auto popover and preserves the current focus. The low-level PopoverOptions.Mode accepts auto (default) and hint; hints do not explicitly focus their anchors. Native autofocus descendants still follow browser behavior, so tooltip content stays text. Keyboard traversal follows each platform's native order. The implementation draws on the APG tooltip draft, which is still a work in progress, and the dismissible/hoverable/persistent requirements for content on hover or focus.

Menu(MenuProps) uses the same anchored layer for a single list of actions. Supply ID, Label, Items, an Open reader, OnOpenChange and OnAction. Item keys must be unique, including separator keys. Actions require labels; the optional Item renderer supplies noninteractive label content. TriggerRef, TriggerClass, Class and the vui-menu-* parts support composition and styling.

Enter, Space and ArrowDown open at the first action; ArrowUp opens at the last. Up/Down wrap, Home/End reach the ends, and typing finds a label using a 700 ms prefix window. Repeated initial letters cycle matches. Disabled actions remain focusable with aria-disabled and cannot activate; separators are skipped. Enter/Space and pointer activation close the menu and dispatch one action key. Native Escape returns focus to the trigger; outside clicks retain their new focus target. Tab exits to the next page control and Shift+Tab returns to the trigger. The trigger explicitly participates in native keyboard order, including WebKit's default button-skipping configuration. Removed active actions repair focus after commit; an empty menu closes and disables its trigger.

This recipe follows the menu button pattern and menu keyboard behavior. Checkbox/radio menu items, submenus, context menus and menubars remain separate recipes. Use form selection controls for values rather than representing a selection as an action menu.

Combobox(ComboboxProps) is an editable text input with manual suggestion selection. Supply ID, Label, a Value reader, OnChange and Options. Each option has a unique nonempty Key, a Label and optional Disabled state. Value is the input text; accepting an option sends its Label to OnChange and its Key to the optional OnSelect callback. Free text remains valid. Applications can validate membership when they require a listed choice.

Typing filters labels using a case-insensitive substring match. Filter can replace that policy, including accepting all options from a caller-owned asynchronous query. Changing Options retains the input; removed or disabled active options lose their highlight. Keyboard handling reads the current value, so batched input and navigation use the latest query. A single active-option ref follows its current binding, avoiding retained refs for every previous query result.

Click or Up/Down opens the list. Up/Down moves through enabled options without wrapping. Enter accepts an active suggestion and prevents that keystroke from also submitting a form. With no active suggestion, Enter keeps native form behavior. Escape dismisses suggestions and preserves text. DOM focus stays in the input; aria-activedescendant identifies the highlighted option, which scrolls into view. Left/Right/Home/End retain native text editing and clear the highlight. Tab leaves the field; candidates stay outside the tab order. Composition events temporarily hide suggestions, and the committed input reopens matching results. These choices use the editable combobox pattern.

Name and Required participate in native forms; Disabled prevents editing and selection. Hint and Error use the same relationships as FormField. OnBlur and InputRef connect validation/touched state and invalid-field focus to the optional forms package:

kit.Combobox(kit.ComboboxProps{
    ID: "place", Name: "place", Label: "Your next stop", Options: places,
    Value: func() string { return form.Value("place") },
    OnChange: func(value string) { form.Change("place", value) },
    OnBlur: func() { form.Blur("place") },
    Error: form.Error("place"), InputRef: placeRef,
})

Class, InputClass, PopupClass and the Option content renderer expose styling. Option content must remain noninteractive. EmptyLabel customizes empty feedback; Label supplies the field and suggestion list's accessible name. The popup follows the input width and stays within the visual viewport. Multi-selection, inline completion and virtualized suggestion lists remain separate capabilities.

Listbox(ListboxProps) consumes an immutable collection.Items snapshot and a live Selected reader. OnSelectionChange receives fresh keys; Multiple enables independent toggles. Arrow/Home/End/typeahead moves the active option, and Space/Enter or a click selects it. Disabled options are skipped. Virtual renders a fixed-row window plus the active option while DOM focus stays on the listbox. Custom Item content must be noninteractive. See the collection contract and standalone window primitive for bounds, filtering, retained data, SSR and custom rendering. Height defaults to 240px, RowHeight to 40px and Overscan to zero; choose an explicit overscan for expected scroll speed.

Table(TableProps) renders a native table with a caption, column headers and optional row headers. Each TableRow has a unique Key and a Cells slice matching the columns; cells retain their column keys. Sort reads the current TableSort, and OnSortChange requests ascending/descending order from the caller. Sorting does not mutate supplied data. Only the current column carries aria-sort; native header buttons keep keyboard behavior. SortHint explains sorting once in the caption, and EmptyLabel describes empty rows. Class styles the scroll container, TableClass styles the table, and each TableColumn.Class styles its cells. Custom interactive cells retain ordinary Tab stops. The behavior follows the APG sortable-table example.

Pagination(PaginationProps) uses one-based pages, a live Page reader and an OnChange callback. PageCount must be positive and Page must be within its range; use one empty page for an empty data set. The caller owns page data and URL state. Previous/next callbacks read the latest page, including within a native event burst. The current page uses aria-current. Neighbors defaults to one and is bounded at five; first/last pages and gaps keep rendered work independent of PageCount. Label, PreviousLabel, NextLabel and PageLabel support localization. Both controls have a Studio example, including notes retained by record key across sorting and pagination. Multi-column sorting, virtual tables and spreadsheet-style grid navigation remain separate features.

ElementRef.ScrollIntoView() queues a nearest-edge scroll after the next commit, without changing focus. Like Focus, it resolves the current binding after reconciliation and ignores missing/disposed bindings. The host applies it after native layer presentation, so a newly shown option can become visible in one commit. The request never starts an animation or runs during SSR rendering. For an explicit container position, ScrollTo and guarded corrections use the same ref lifetime, without changing focus.

The behavior follows the HTML dialog contract and the WAI-ARIA modal dialog pattern. Keyboard, focus, cancellation and composition need executable browser evidence; native element use alone does not establish full accessibility conformance. Modal backdrop dismissal, exit transitions and arbitrary portals are separate capabilities. Browser and assistive-technology coverage follows the main rewrite plan.

Native keyboard and customization contracts#

Tabs, Menu, Listbox, Combobox and ToastRegion reserve unmodified keys for their documented navigation, selection and dismissal actions. Alt, Control, Meta and Shift combinations keep their native default action; the recipes do not reinterpret them as plain arrows or selection keys. Composition retains the existing native behavior. Generic EventOptions still permits callers to choose their own exact modifier filters.

Tabs accepts Class for a scoped application recipe and Title func(TabHeading) ui.View for custom noninteractive heading content. TabHeading contains Key, Label, Selected and Disabled. The declared Label remains the accessible name, including when Title draws only decorative content. Existing vui-tab-list, vui-tab-panel and role selectors expose its parts to application CSS.

Menu accepts Trigger func(bool) ui.View, receiving its current presented-open state. It supplies noninteractive content inside the existing native button; Label remains the accessible name. The same button retains its ref, keyboard behavior, disabled handling and popup relationship, including with a custom icon or changing label content. This matches the existing Popover composition.

Each keyed Accordion item owns its default trigger ref. Removing an item disposes that owner; replacement keys do not accumulate refs on the containing Accordion. Collapsed and reordered items retain their content and focus contract. A supplied TriggerRef keeps its caller's existing scope and ownership.

Built with Volang UI · A work in progress, made to be explored.
Opening Studio…