@sigitex/glim
v1.10.0
Published
Compact, tactile UI components for SolidJS
Readme
Glim for SolidJS
Compact tactile UI components, scoped design tokens, sizing tokens, dark and light palettes, and five procedural surface textures.
This README sucks!
ToggleButton
ToggleButton is a native button with persistent on/off state exposed through aria-pressed. It accepts Button variants, sizes, native attributes, and events. It defaults to unpressed and type="button".
<ToggleButton defaultPressed>Bold</ToggleButton>
<ToggleButton pressed={pinned()} onPressedChange={setPinned} size="sm">
Pin
</ToggleButton>Use defaultPressed for local state or pressed with onPressedChange for controlled state. Disabled buttons do not toggle. Calling preventDefault() in onClick cancels the toggle. Keep the label stable as state changes. Use Switch for a boolean form field instead.
Drawer
Drawer supports all four edges, native modal or modeless behavior, overlay or scoped push layout, and pointer/keyboard resizing. Its default surface is window; surface also accepts panel, inset, group, raised, and overlay.
import { createSignal } from "solid-js";
import { Drawer, DrawerLayout, Button, Surface } from "@sigitex/glim";
function Inspector() {
const [open, setOpen] = createSignal(false);
const [size, setSize] = createSignal(320);
return (
<DrawerLayout style={{ height: "100dvh" }}>
<Surface style={{ height: "100%" }}>
<Button onClick={() => setOpen(true)}>Project details</Button>
{/* Application content */}
</Surface>
<Drawer
title="Project details"
description="Settings for the current project."
open={open()}
onOpenChange={setOpen}
placement="right"
layout="push"
modal={false}
resizable
size={size()}
onSizeChange={setSize}
defaultSize={320}
minSize={160}
maxSize={600}
surface="window"
footer={<Button onClick={() => setOpen(false)}>Done</Button>}
>
Project settings
</Drawer>
</DrawerLayout>
);
}placement defaults to right; layout defaults to overlay; modal, closeOnEscape, closeOnOutsideClick, and showCloseButton default to true. Modal drawers use native focus trapping and document inertness; non-modal drawers leave the application interactive. Escape dismisses only the foremost Drawer or Dialog. Outside dismissal requires a press and click outside, so releasing a resize drag over the page does not dismiss it. Closing restores the opener's focus; outside clicks in non-modal mode preserve focus on the clicked control.
size / onSizeChange control the size, or initialize an uncontrolled drawer with defaultSize (320). Sizes, minSize (160), and maxSize are CSS pixels: width for left/right, height for top/bottom. The available viewport or visible layout bounds always constrain the final size. resizable defaults to false. Focus the exposed resize edge and use arrows to grow/shrink, Shift for ten times resizeStep (8), Home/End for limits, or double-click to restore defaultSize. onResizeEnd reports committed pointer/keyboard resizing; pointer cancellation restores the starting size.
Drawers are flush to viewport edges by default, including inside a DrawerLayout. offset adds an optional outer gutter in CSS pixels (default 0); contained opts into the visible bounds of a DrawerLayout (default false). DrawerLayout defines the content affected by push mode. Push mode requires this ancestor and reserves space on the corresponding edge without altering the document body. Reservations follow size/placement changes and clear on close or unmount. Give contained layouts an explicit height. Contained drawers also fall back to viewport edges when no DrawerLayout is present. Only the drawer body scrolls; header and optional footer stay visible. class and style are supported on both primitives; explicit styles override Drawer's generated geometry. Prefer size and placement props for geometry that should remain synchronized with push-layout reservations. Opening and closing slide the drawer its full width or height over 250ms, without fading the panel or backdrop. Push space follows the visible drawer throughout the slide instead of jumping to its final size. Modal focus remains active until the exit finishes. Both animations are skipped for reduced motion.
For page-wide push behavior, wrap the page—not just the drawer preview—in DrawerLayout. Push mode uses a transparent backdrop, including when modal is true; focus trapping and document inertness still apply. Overlay mode retains its dimming backdrop.
Install
Install the downloaded package with npm install ./sigitex-glim-0.11.0.tgz. It requires SolidJS 1.9. Use Solid's Vite plugin or another Solid JSX compiler.
LED And LEDBar
LED is a recessed indicator lens, off by default. on lights it; tone defaults to success, or set color to a CSS color. Sizes are xs (6px), sm (10px), md (16px, default), lg (24px), and xl (32px), shared by LEDBar. Emitted glow is enabled by default; disable it with glow={false}. A label exposes the light and its on/off state to assistive technology; unlabelled LEDs are decorative, not interactive controls.
LEDBar composes LEDs into determinate progress with accessible numeric bounds. min defaults to 0, max to 100, value to the minimum, and segments to 20 (clamped to 1–100). Only completed segments light, while assistive technology receives the exact clamped value. It shares LED appearance props, defaults to the accent tone, and shows a percentage unless showValue={false}. Neither component animates or blinks.
The bar has no housing, background, or border. orientation defaults to horizontal (left-to-right); vertical fills bottom-to-top with the label and readout above. Optional colorStops={[{ at: 0, color: "var(--g-success)" }, { at: 70, color: "var(--g-warning)" }, { at: 90, color: "var(--g-error)" }]} creates discrete color zones. at is a percentage of the range, independent of min/max; LEDs use their segment's starting percentage. Both lit and unlit lenses retain their zone color. Stops are sorted without changing the supplied array, finite positions clamp to 0–100, and later stops win at duplicate positions. Invalid positions and empty colors are ignored. Below the first stop, or without stops, color/tone applies.
<LED label="Power" on={powered()} tone="success" />
<LED on color="#ff9900" size="lg" glow={false} />
<LEDBar label="Charge" value={charge()} segments={20} tone="success" />
<LEDBar label="Signal" min={-60} max={0} value={-12} showValue={false} />
<LEDBar label="Level" value={level()} orientation="vertical" colorStops={[{ at: 0, color: "var(--g-success)" }, { at: 70, color: "var(--g-warning)" }, { at: 90, color: "var(--g-error)" }]} />Mouse Wheel
Hover the tab strip and scroll down/right for the next enabled tab, or up/left for the previous one. Wheel switching skips disabled tabs and stops at the first or last enabled tab without wrapping; at either end, normal scrolling continues. Scrolling panel content does not switch tabs. Focus follows selection only if focus was already in the tab strip.
Hover a Slider or Knob (including its label/readout) and scroll up/right to increase or down/left to decrease by one step per wheel event. This also works for vertical and specialized sliders. Values respect their bounds; disabled controls, zoom/modifier gestures, and gestures with no delta are ignored. Handled gestures prevent page scrolling without taking focus; at range limits, normal scrolling continues. Native onWheel handlers can cancel adjustment with preventDefault().
Knob
Knob is a rotary numeric range control. Compare variant="instrument" (default, recessed dial and indicator groove), minimal (shallow disc and recessed dimple), and console (dark domed face, raised outer housing, ivory pointer, and dot scale). Instrument and minimal use neutral inset indicators; console uses an ivory pointer extending inward from the rim. All share the same behavior.
The optional value arc sits between the face and scale ticks. Instrument seats it in a recessed well with matching inset shading; minimal keeps only the value arc, without a backing rail, well, or inset shading. Console defaults to no arc; set showArc to add a value arc in its outer ring without a backing rail. At the range minimum, the value stroke is omitted entirely. Hide the arc with showArc={false}. Scale ticks sit outside the arc and can be controlled independently with showTicks. Ticks default to visible for instrument and console, and hidden for minimal; console uses dots rather than lines. Its dots and labels become dark in light mode; the pointer stays ivory. Sizes are xs (36px), sm (56px), md (88px, default), lg (128px), and xl (168px). These dimensions describe the dial, excluding its label and readout.
Set showMinMax to show small, muted endpoint labels close to the lower sides of the dial. It defaults to false, includes unit when supplied, and works independently of the arc, ticks, and current-value readout. The labels are decorative; the native range already exposes its bounds to assistive technology. Keyboard focus adds Glim's shared glow to the visible dial face and its material shadows, not the larger input hit area. Forced-colors mode retains its system outline on the face.
<Knob label="Gain" variant="instrument" defaultValue={40} unit="%" />
<Knob label="Mix" variant="instrument" value={mix()} onValueChange={setMix} />
<Knob label="Pan" variant="minimal" min={-1} max={1} step={0.1} defaultValue={0} />
<Knob label="Threshold" variant="console" min={-60} max={0} defaultValue={-18} unit=" dB" />
<Knob label="Tinted" variant="console" hue={260} intensity={40} />
<Knob label="Trim" size="xs" showArc={false} showTicks={false} />
<Knob label="Master" size="xl" showMinMax />All variants accept optional hue (degrees, wrapping at 360) and intensity (tint strength from 0 to 100, not brightness). Omit them to preserve each variant's existing appearance exactly. Intensity 0 removes material saturation; hue alone keeps its existing strength. These adjust the material's hue and saturation while retaining lightness, relative shade differences, geometry, and lighting. The accent arc, focus glow, text, ticks, and console pointer retain their existing colors. Set --g-knob-hue and --g-knob-intensity on an ancestor for CSS theme overrides; explicit props override the inherited values. Instrument/minimal use the theme's neutral hue/intensity as their baseline. Console preserves its approved palette with baseline hue 180 and intensity 20 in both modes; supplying those values is also an identity adjustment.
Console's hardware stays dark in light mode: face, housing, reflections, bevel, and ivory pointer do not change. Only the surrounding dots and text adapt to the theme. Optional lightness (0–100) sets the console face's base HSL lightness and shifts the other material shades by the same amount, clipping at black/white. Omit it to preserve the approved material (base lightness approximately 11.37). This changes material colors, not a brightness filter; lighting geometry, pointer, text, arc, and focus glow remain unchanged. For example, <Knob label="Gain" variant="console" lightness={25} /> uses lighter hardware in either mode. --g-knob-lightness supplies an inherited CSS override; an explicit prop takes precedence. Instrument and minimal accept but ignore lightness.
Drag upward to increase and downward to decrease; hold Shift for fine adjustment. A native range input supplies keyboard controls, accessible slider semantics, disabled behavior, and form submission via name. min, max, step, value, defaultValue, onValueChange, showValue, and unit follow Slider conventions. The defaults are 0–100, step 1, and value 40. Values clamp and snap to the step. Native attributes, refs, and input events apply to the underlying input. style targets the wrapper; inputStyle targets the native input. Pointer gestures do not jump the value on press; cancellation, disabling, and unmounting clean up dragging.
Slider Styles And Sizes
Slider defaults to variant="standard" and size="md", preserving the original 13×18px horizontal thumb and 160px vertical travel area. Choose variant="fader" for a raised rectangular cap with notched grip faces and an inset center mark. variant="console" has a more compact mixer cap: two smooth sloping faces, flared end lips, and a narrow center seam, without grip notches. Both material caps use Glim's grain texture. The rail stays recessed, without an accent value fill. All styles preserve the native range input, keyboard, wheel, form, and disabled behavior.
Console uses a wide, near-black recessed slot with rounded ends and subtle edge lighting. Its vertical caps measure 14×20px (sm), 22×32px (md), and 34×50px (lg); horizontal caps rotate those dimensions.
All variants and specialized sliders accept guide, off by default. guide={true} adds five evenly spaced numeric labels from min to max, plus minor ticks. Labels include unit when supplied. Use guide={[{ value: 0, label: "Quiet" }, { value: 80, label: "Nominal" }, { value: 100, label: "Max" }]} for custom labeled marks; omitted labels use the value and unit. Non-finite, out-of-range, and duplicate values are ignored. An empty guide renders nothing. Guides sit below horizontal controls and to the right of vertical controls, aligned to thumb-center travel rather than the full slot length. Guides are decorative, do not change the numeric scale or step, and are hidden from assistive technology. The demo's “Scale guides” checkbox toggles guides across every style, size, and orientation.
| Size | Standard cap | Fader cap | Console cap | Vertical input length |
| --- | --- | --- | --- | --- |
| sm | 8×12px | 22×14px | 18×12px | 100px |
| md | 13×18px | 34×22px | 26×18px | 160px |
| lg | 22×32px | 56×36px | 42×28px | 260px |
Cap dimensions are horizontal width×height; vertical caps and grip direction rotate. Horizontal length follows the container. inputStyle can override input dimensions; coarse pointers retain at least a 44px cross-axis hit area. All Slider specializations inherit both variant and size.
<Slider label="Volume" variant="fader" orientation="vertical" size="lg" defaultValue={60} />
<Slider label="Channel" variant="console" orientation="vertical" defaultValue={60} />
<Slider label="Mix" size="sm" defaultValue={40} />Slider Specializations
Slider accepts orientation="horizontal" (default) or orientation="vertical". Vertical sliders increase from bottom to top, retain native keyboard controls, and place the value below the track. Labels stay horizontal. The medium vertical input defaults to 160px tall; override its height with inputStyle={{ height: "240px" }}. All three specializations accept orientation and adjust their gradients to match.
<Slider label="Volume" orientation="vertical" defaultValue={40} />
<HueSlider orientation="vertical" />HueSlider, IntensitySlider, and TextureStrengthSlider are independent wrappers composing Slider; they do not read or change ThemeProvider. They accept all Slider props, with optional labels. Hue defaults to 0–360, value 205 and °; intensity defaults to 0–100, value 0 and %; texture strength defaults to 0–100, value 35 and %. All defaults can be overridden.
<HueSlider value={hue()} onValueChange={setHue} />
<IntensitySlider hue={hue()} value={intensity()} onValueChange={setIntensity} />
<TextureStrengthSlider texture="brushed-metal" value={strength()} onValueChange={setStrength} />IntensitySlider accepts hue?: number (default 205). TextureStrengthSlider accepts texture?: SurfaceTexture (default fine-grain). The selected texture is previewed from smooth to full strength inside the rail. The base Slider stays neutral. Its trackStyle?: JSX.CSSProperties styles the track container; set --g-slider-track-background to customize the rail while retaining its bevel and thumb. Caller track styles override wrapper effects. style targets the wrapper; inputStyle targets the native input.
Progress
Progress defaults to a recessed continuous fill. Use variant="segmented" for individual tactile cells, with segmentCount (default 20, clamped to 1–100). Partial cells preserve the exact value. Both styles accept label, value, max, tone, and showValue. Omit value for indeterminate progress; no numeric value is announced. Segmented indeterminate progress respects reduced-motion preferences. Invalid or nonpositive max falls back to 100; determinate values are clamped to the range. Completion does not automatically change tone.
<Progress label="Upload assets" value={62} />
<Progress label="Upload assets" value={62} variant="segmented" segmentCount={20} />
<Progress label="Build assets" value={100} tone="success" />
<Progress label="Working" variant="segmented" />Status
Status renders a compact status label with an optional dot. It accepts children, tone (default neutral), and dot (default true).
<Status tone="success">Active</Status>
<Status dot={false}>Modified</Status>import { createSignal } from "solid-js";
import { ThemeProvider, Button, Select } from "@sigitex/glim";
export function App() {
const [status, setStatus] = createSignal("active");
return (
<ThemeProvider mode="dark" hue={205} intensity={20}>
<Select
label="Project status"
name="status"
options={[
{ value: "active", label: "Active" },
{ value: "paused", label: "Paused" },
]}
value={status()}
onValueChange={setStatus}
/>
<Button variant="primary" onClick={() => console.log(status())}>
Save
</Button>
</ThemeProvider>
);
}State and composition
- String and number controls:
value/onValueChange, ordefaultValuefor local state. - Boolean controls:
checked/onCheckedChange, ordefaultChecked. - Dialog:
open/onOpenChange, ordefaultOpen.modaldefaults totrue;draggabledefaults tofalse;closeOnEscapedefaults totrue. - Pagination:
page/onPageChangeandpageSize/onPageSizeChange. - Accordion: string arrays through
value/onValueChange, ordefaultValue. - Table is controlled: provide rows, columns, rowKey, and optional selection/sort callbacks. Filtering, sorting, fetching, and paging remain app concerns.
- Tag removal calls
onRemove; update your tag collection in that callback. FormFieldaccepts a render function supplying the control ID, descriptions, validation, and required/disabled props. Spread them ontoInputorSelect.- Wrap notification consumers with
ToastProvider, then calluseToast().show(message, {tone, duration, action}). A zero duration remains visible until dismissed. The provider supports six viewport placements, spacing, a visible-stack limit with queued delivery, and hover/focus pausing. Useupdate(id, patch)to change an existing notification.
Positioned notifications
<ToastProvider
placement="top-right"
offset={18}
gap={8}
maxVisible={3}
pauseOnHover
pauseOnFocus
>
<App />
</ToastProvider>placement accepts top-left, top-center, top-right, bottom-left, bottom-center, and bottom-right (default). offset is the outer gutter in CSS pixels (18 by default); gap is the spacing between notifications (8). Gutter sizes adapt to narrow viewports. maxVisible defaults to 3, with a minimum of 1. The oldest notifications appear first. Additional entries wait in a FIFO queue and start their timers only when they become visible. Reducing the limit requeues extra entries; their duration starts afresh when displayed again.
Hover and keyboard focus pause dismissal by default and preserve the remaining time. Leaving one action for another inside the same toast keeps it paused. Set pauseOnHover or pauseOnFocus to false on the provider to disable either behavior. Standalone Toast components expose the same pause options. Individual notifications retain tone, duration (4500ms by default; 0 is persistent), dismissible, and an optional action.
const toast = useToast();
const id = toast.show("Saving…", { duration: 0, tone: "accent" });
// After saving:
toast.update(id, {
message: "Saved",
tone: "success",
duration: 4500,
});
// Or remove it, even while queued:
toast.dismiss(id);show returns an ID. dismiss removes a visible or queued entry. update(id, patch) returns false if that ID no longer exists; otherwise it updates the same notification, including queued entries. IDs and visible DOM elements stay stable, so action focus is retained. Changing message, tone, action, or dismissible does not reset its clock. Changing duration restarts the countdown; setting duration to 0 cancels automatic dismissal. Passing action: undefined removes an action. Provider class applies to its notification region. Timers are cleaned up when notifications or the provider unmount.
Icons default to the tone's status symbol. Pass icon to show, update, or standalone Toast: JSX replaces the automatic icon, false hides its entire frame, and undefined restores the automatic icon. Icons are decorative and inert; interactive content belongs in the action slot. SVG icons fit the 14px inset icon space.
toast.show("Custom status", { icon: <MyIcon /> });
toast.show("Message only", { icon: false });
toast.update(id, { icon: undefined });New notifications slide in for 200ms without an opacity fade. Left/right placements enter horizontally; centered placements enter vertically. In-place updates do not replay the entrance. Reduced motion disables the slide.
Tabsitems contain{value, label, content: () => JSX.Element}. By default, inactive panel content unmounts. SetkeepMountedto mount all panel content and hide inactive panels without losing local state. Effects in hidden panels keep running.variant="classic"joins the active tab to its content;separateadds space between them.- Breadcrumb items support native links, app navigation callbacks, sibling menus, searchable menus, and responsive middle-level collapse.
separatoraccepts text, icons, or other JSX between items, including the collapsed menu. It defaults to/;null,false, or an empty string hides separators without spacing. Separators are decorative and hidden from assistive technology.
Standard Solid DOM attributes and events pass through Button, Input, Textarea, Surface, Checkbox, Switch, and Slider. Select's custom trigger uses a native hidden select for form submission. Labels should be supplied explicitly for icon-only and custom controls.
On a focused, closed Select, arrow keys change the selection without opening its popup, skipping disabled options and stopping at either end. Home/End select the first/last enabled option. Click, Enter, Space, or Alt+ArrowDown opens the popup; while open, Up/Down moves option focus and Enter confirms.
Component Styles
Every visual component accepts style. It targets the outer visible element, except overlays, where it targets the visible panel rather than the anchor or backdrop. Styles update reactively; explicit styles override generated inline defaults, including positioning. Removing an override restores the generated default. Overriding geometry can bypass built-in layout constraints.
| Component | style target | Additional style props |
| --- | --- | --- |
| Table | Scroll wrapper | tableStyle for the table |
| Slider and specializations | Outer label | inputStyle, trackStyle |
| Knob, NumberInput | Outer wrapper | inputStyle |
| Checkbox, Switch | Label when supplied, otherwise input | inputStyle (wins on the unlabeled input) |
| Popover, Tooltip, Menu, ContextMenu, TableColumnControls | Floating panel | anchorStyle for the anchor wrapper |
| Dialog, Drawer | Visible dialog panel | — |
| Select | Outer wrapper | triggerStyle, panelStyle |
| Menubar | Menu bar | panelStyle for all popups |
| Toast | Notification | Also accepted by toast.show and toast.update |
| ToastProvider | Notification viewport | — |
Use Solid JSX.CSSProperties objects with hyphenated CSS names. Components derived from native DOM props also retain string-style support. CSS custom properties are supported throughout.
Breaking change: existing style values intended for the native input on Slider, its specializations, Knob, NumberInput, or labeled Checkbox/Switch must move to inputStyle. Other native attributes and events retain their existing targets.
<Slider label="Volume" orientation="vertical" style={{ "margin-inline": "12px" }} inputStyle={{ height: "240px" }} />
<Popover label="Details" style={{ width: "280px" }} anchorStyle={{ display: "inline-block" }}>Details</Popover>Component Surfaces
Dialog, Popover, Tabs, Accordion, Toolbar, Menubar, and Toast accept surface: panel, window, inset, group, raised, or overlay, matching Surface variants. Omitting it preserves each component's existing appearance.
The option styles Dialog's shell and body, Popover's floating panel, Tabs' content panels, Accordion's expanded bodies, Toolbar's container, Menubar's bar (not its popups), and each Toast. Classic Tabs match the selected tab to the panel surface; variant="separate" only changes the panels. Surface changes do not remount content.
ToastProvider surface="overlay" supplies a default. Override it per notification with toast.show("Saved", { surface: "raised" }), or change it through toast.update(id, { surface: "inset" }). Setting a notification's surface to undefined restores the provider default. Drawer retains its existing surface="window" default.
<Dialog title="Settings" surface="window">Settings content</Dialog>
<Popover label="Details" surface="raised">Details content</Popover>
<Toolbar label="Tools" surface="inset"><Button>Save</Button></Toolbar>Theme
ThemeProvider scopes its variables to a wrapper. Use multiple sibling providers for independent themes. Defaults reproduce the original Glim palette: dark hue 205, intensity 20, texture strength 35; light hue 205, intensity 12, texture strength 15. Size and spacing are controlled directly by CSS tokens: --g-control (28px), --g-row (32px), --g-gap (6px), --g-control-pad (2px), and --g-row-pad (4px).
Textures: fine-grain, brushed-metal, polished-metal, bead-blasted-metal, paper-fiber. Strength is 0–100; zero disables texture.
Override tokens with tokens={{'--g-radius':'2px', '--g-font-family':'"Source Sans 3", sans-serif'}}. The component package does not load fonts. The demo includes Source Sans 3 under the SIL Open Font License.
Components import plain CSS automatically, including shared theme, focus, and native scrollbar foundations. Source modules and compiled ESM retain separate component imports, so a bundler can omit unused components and their CSS. Import your application styles after Glim. Demo layout styles are separate. Overlays use the native popover top layer, and Dialog uses native dialog focus containment when modal. Use current browsers supporting these APIs.
Exports
The solid condition provides TSX source for Solid-aware compilation, including server rendering. The import condition provides the compiled browser ESM library. Solid is a peer dependency, never bundled. CSS and declarations ship alongside the source and browser output. Package creation does not publish to an npm registry.
Component declarations in lib/index.d.ts document the complete typed API.
For a Vite SSR build, configure solid({ ssr: isSsrBuild }) in your Vite configuration callback. This selects Solid's server JSX generation for the package source as well as your app.
Keyboard shortcuts
Keys displays shortcut keycaps without registering keyboard listeners.
<Keys keys={["Ctrl", "Shift", "P"]} />
<Keys keys={["↑", "↓"]} separator="" aria-label="Up or down arrow" />Working surfaces
<SplitPane
label="Resize inspector"
defaultSize={76}
min={55}
max={85}
first={<ProjectList />}
second={<Inspector />}
/>size / onSizeChange makes the split controlled. defaultSize is uncontrolled. Sizes and limits are percentages of available pane space. orientation="vertical" stacks panes. The divider supports pointer dragging, arrows, Shift+arrows, Home/End, and Enter or double-click to reset; onResizeEnd reports completed changes. Give vertical splits a definite height. Pane contents stay mounted while resizing. Set disabled to lock a divider.
Compose Toolbar with ToolbarButton, ToolbarGroup, ToolbarSeparator, and ToolbarSpacer. Give it a label. It uses roving focus for actions, skips disabled buttons, and leaves text-input and select keys alone. Set orientation="vertical" for a vertical toolbar. Optional overflowItems and onOverflowAction add a More actions menu; the application chooses which actions go there. The toolbar wraps on narrow screens.
<ContextMenu
label="Project actions"
items={[
{ label: "Edit", value: "edit" },
{ label: "Duplicate", value: "duplicate" },
{
label: "Delete",
value: "delete",
separatorBefore: true,
destructive: true,
},
]}
onAction={handleAction}
>
<ProjectCard />
</ContextMenu>Context menus open by right-click, the Context Menu key, or Shift+F10. Menus stay within the viewport, support arrows, Home/End and typeahead, and return focus on Escape or selection. Outside clicks and scrolling dismiss them. disabled or Shift+right-click preserves the browser context menu. Use onOpenChange to observe menu visibility. Keep an ordinary action button/menu available for touch users, as the demo does.
Dialog windows
<Dialog
open={settingsOpen()}
onOpenChange={setSettingsOpen}
title="Project settings"
modal={false}
draggable
>
<ProjectSettings />
</Dialog>Omit modal for a blocking modal dialog. Set modal={false} to keep the page interactive. Dragging is opt-in: pass draggable to move the title bar with a pointer or focus it and use arrows (Shift for larger steps; Home centers). Positions are constrained to the viewport. Both options can change while open without remounting content.
Set closeOnEscape={false} to disable Escape dismissal in either mode. This option can change while open. Close buttons and other dismissal actions remain available. A foremost window with Escape disabled also shields windows behind it from the same keypress.
Render multiple Dialog instances with independent open state. Modeless windows come forward on focus or pointer interaction; Escape closes the foremost window. Modal dialogs use the browser’s native top layer and focus containment. Closing restores focus to the opener when it is still available.
Menubar
<Menubar
label="Application menu"
menus={[
{
label: "File",
items: [
{ label: "Save", value: "save", shortcut: "Ctrl+S" },
{
label: "Export",
children: [
{ label: "PDF", value: "export-pdf" },
{ label: "Markdown", value: "export-md" },
],
},
],
},
]}
onAction={handleAction}
/>Top-level menus support disabled; items support disabled, separatorBefore, destructive, shortcut, and nested children. Use type="checkbox" or type="radio" with controlled checked; update application state in onAction. Selecting an action closes menus and restores focus. Shortcut labels do not register global keyboard bindings.
The menubar has a single Tab stop with Left/Right and Home/End navigation. Down/Up opens the first/last item. Menus support Up/Down, Home/End, typeahead, nested Right/Left navigation, and Escape one level at a time. Tab exits; outside clicks dismiss. Hover switches menus once one is open. Popups use the native top layer and stay within the viewport. Sizing, themes, and textures use the shared tokens.
Toolbar actions have a quiet resting state, raised hover, and inset pressed/selected states. Primary buttons keep their accent; joined ButtonGroups keep their existing appearance. Use <ToolbarButton icon aria-label="Bold" aria-pressed={bold()} onClick={toggleBold}>B</ToolbarButton> for square icon actions. Ordinary Buttons also get toolbar styling inside a Toolbar. Overflow uses an icon trigger with overflowLabel as its accessible name.
Toolbar spacing follows the sizing tokens through independently overridable tokens: --g-toolbar-gap, --g-toolbar-group-gap, --g-toolbar-separator-gap, and --g-toolbar-button-pad. Group gaps remain between 0 and 2px by default.
Button labels use a small optical offset for Source Sans 3: --g-button-text-offset: 0.1em (about 1px for small labels, 1.4px at 14px). It applies to regular, toolbar, and joined buttons without changing their outer dimensions. Override it through ThemeProvider tokens, including 0px for fonts that need no correction.
Tree view
<TreeView
label="Files"
items={[
{
id: "src",
label: "src",
children: [
{ id: "app", label: "App.tsx" },
{ id: "styles", label: "styles.css" },
],
},
]}
value={selected()}
onValueChange={setSelected}
defaultExpanded={["src"]}
checkable
checked={checkedFiles()}
onCheckedChange={setCheckedFiles}
/>Node IDs must be unique throughout a tree. Each node supports label, children, disabled, and an optional JSX icon. A disabled branch disables its descendants. Selection uses value / onValueChange or defaultValue; expansion uses expanded / onExpandedChange or defaultExpanded. Selection and keyboard focus are separate: Enter or a row click selects.
Enable checkable for hierarchical checks. checked / onCheckedChange or defaultChecked contain leaf IDs. Checking a branch toggles all enabled descendant leaves, including collapsed children; disabled leaves are excluded. Parent states are derived as checked, unchecked, or mixed. Checks do not change selection.
Up/Down moves through visible enabled rows; Right expands or enters a branch, Left collapses or returns to the parent. Home/End jumps to the first/last row. Space checks (or selects when checks are disabled); typeahead finds visible labels. The tree has one Tab stop. Set disabled to disable the whole tree. Override --g-tree-indent to change indentation.
Number input
<NumberInput
label="Quantity"
value={quantity()}
onValueChange={setQuantity}
min={0}
max={20}
step={0.25}
/>The numeric value is number | null; null represents an empty field. Use value / onValueChange or defaultValue. In-range edits update the value immediately while preserving partial numeric drafts. Blur or Enter commits, clamps to bounds, and normalizes the displayed number; invalid text reverts to the current value. Decimal steps avoid common floating-point display artifacts. Step sizes are increments, rather than a validation lattice.
Up/Down steps, Shift multiplies the step by ten, and Home/End goes to a supplied bound. Escape cancels an unfinished draft. Optional hideSteppers removes the pointer controls while retaining keyboard support. disabled, readOnly, required, name, id, standard input attributes, onBlur, and onKeyDown pass through. The native named input submits its displayed value; commit it before programmatic form submission if an edit is still unfinished. FormField can supply its label and validation attributes. label provides a direct accessible name when needed.
Table column preferences
const [widths, setWidths] = createSignal<Record<string, number>>({});
const [order, setOrder] = createSignal<string[]>([]);
const [hidden, setHidden] = createSignal<string[]>([]);
const columns: TableColumn<Project>[] = [
{ key: 'name', label: 'Project', cell: p => p.name,
width: 180, minWidth: 110, sticky: 'start' },
{ key: 'owner', label: 'Owner', cell: p => p.owner, width: 140 },
{ key: 'status', label: 'Status', cell: p => p.status,
width: 120, sticky: 'end' },
];
<TableColumnControls
columns={columns}
order={order()} onOrderChange={setOrder}
hidden={hidden()} onHiddenChange={setHidden}
/>
<Table
label="Projects" rows={projects()} columns={columns} rowKey={p => p.id}
resizable reorderable stickyHeader maxHeight="360px"
columnWidths={widths()} onColumnWidthsChange={setWidths}
columnOrder={order()} onColumnOrderChange={setOrder}
hiddenColumns={hidden()}
/>All existing table sorting, selection, row actions, and empty states remain available. New behavior is opt-in. Column keys must be unique. Widths are pixels; resizing defaults to a 160px width with 80–640px bounds. Override width, minWidth, and maxWidth per column, or set resizable: false to lock it. Resize by dragging the header edge, or focus its separator and use Left/Right (5px), Shift+arrows (20px), and Home/End for limits. Double-click resets to the column's configured width. Pointer resize listeners are removed when resizing ends or the table unmounts.
columnWidths / onColumnWidthsChange and columnOrder / onColumnOrderChange are controlled; defaultColumnWidths and defaultColumnOrder provide local defaults. hiddenColumns is an app-owned list; defaultHiddenColumns supplies an initial configuration. Use TableColumnControls to edit visibility and order through shared application state. It keeps the final column visible. If a caller hides every column, Table displays the first available column as a fallback. Unknown order keys are ignored, duplicates removed, and newly added columns appended.
With reorderable, drag a header grip to another header, or focus it and use Left/Right. TableColumnControls offers pointer-friendly earlier/later actions. sticky: 'start' and 'end' pin columns to logical edges, including RTL. Pinned columns stay grouped at their edge; reordering works within each pinned/unpinned region. Offsets recalculate after hiding or resizing. When start columns are pinned, the row-selection column is pinned too.
stickyHeader and maxHeight enable a vertically scrolling header. Configured widths, resizing, or pinned columns enable fixed layout and horizontal scrolling; widths remain literal pixels rather than stretching to fill the viewport. class and style target the scroll wrapper; tableStyle targets the table itself. Sorting, data operations, and persistence remain application responsibilities; store column state wherever your app keeps preferences.
Header grips use pointer capture for dragging, including touch, and highlight compatible drop targets. A single column in a pinned region has a disabled grip because it has nowhere to move within that region. Change the column's sticky option to unpin it before moving it into another region; the advanced demo provides a Pin edge columns toggle.
Lists
<List
label="Project files"
size="md"
selectionMode="multiple"
value={selectedFiles()}
onValueChange={setSelectedFiles}
checkboxes
dividers
>
<ListItem
value="app"
label="App.tsx"
description="Application entry"
icon={<FileIcon />}
trailing={<Status>Modified</Status>}
/>
<ListItem value="styles" label="styles.css" disabled />
</List>One List API covers plain and selectable lists. selectionMode defaults to none and keeps native ul/li semantics, with freely interactive trailing buttons, menus, or links. single and multiple use listbox/option semantics and manage keyboard focus. Selection always uses string arrays through value / onValueChange or defaultValue; single mode reads at most one key. Give selectable ListItems stable, unique value keys.
Sizes are sm, md (default), and lg. The List supplies the default; individual ListItems can override size. Size sets row proportions and icon space; shared sizing tokens tune padding and height within each size. Primary/secondary text, icons, trailing decorations, dividers, separatorBefore, disabled states, and custom row content share the same styling. Text wraps instead of being clipped. class and style are available on both primitives.
Selectable lists have one Tab stop. Up/Down, Home/End, and typeahead move focus; Space or Enter selects. In multiple mode, unmodified clicks and Space toggle keys, Shift+arrows or Shift+click select a range, and Ctrl/Cmd+A selects all enabled rows. Ctrl/Cmd+Shift adds a range. Disabled rows are skipped. Selection does not automatically follow arrow-key focus. Optional checkboxes show decorative checks in multiple mode; the option's aria-selected remains the source of accessibility state.
Selectable row content should be descriptive rather than independently interactive. Trailing content is inert and hidden from the accessibility tree in selectable modes; labels and descriptions name each option. Use plain mode for rows with action buttons or other interactive content. Disabled plain rows make their contents inert. Icons are decorative. Removing or disabling the focused option transfers focus to an available option while focus remains inside the list.
