API reference
Every export of @sigx/lynx-zero v0.35.0: the components, the overlay system, axis push-down, press feedback, the theme engine, layout, the zero contract it re-exports, and the ./testing and ./screen-theme subpaths.
Components
Each component has its own page with parts, props and events. They are compounds: the root is callable as the component itself (<Button> is <Button.Root>), and the parts hang off it.
Actions and disclosure
- Button: a pressable action, with
loadingand a spinner part. - Toggle: a button with one bit of state (
on/off). - ToggleGroup: toggles under one value model, single or
multiple. - Tabs: a tab list with panels and a self-positioning indicator.
- Accordion: collapsible sections.
Overlays (render in ZeroRoot's outlet)
- Dialog: a modal panel with backdrop, close and cancel parts.
- Popover: an anchored popup with light dismiss.
- Toast: toasts from a
Toasterstore, in a viewport, with promise toasts.
Form controls
- Field: label, description and error around a control, sharing its flags.
- Fieldset: groups controls and states
disabled/readonly/invalidonce. - Input: a text field on the native
<input>, with adornments and triggers. - Textarea: a multi-line field on the native
<textarea>, with autosize. - NumberInput: a spinbutton with increment and decrement triggers.
- Checkbox: a boolean box, or one member of a
string[]model. - CheckboxGroup: boxes sharing a
string[]model, with a tri-state parent box. - RadioGroup: one choice from a set.
- Switch: an on/off control.
- Select: an items-driven picker on zero's collection core.
- Slider: a touch-driven range, one or more thumbs.
Content
- Card: a surface with media, header, body and footer bands.
- Alert: a dismissable message.
- Badge: a standing label with an optional status dot.
- Status: a presence dot.
- Kbd: a keycap.
- Divider: a rule, optionally labelled.
- Stats: figures with titles, values and descriptions.
- EmptyState: an icon, a message and actions for an empty view.
- Avatar: an image with a fallback.
- AvatarGroup: overlapping avatars with an overflow count.
- Progress: a determinate or indeterminate bar.
- Skeleton: a loading placeholder that keeps its children mounted.
- Spinner: an indeterminate busy mark.
- Timeline: items with markers, connectors and content.
Theme and layout
- ZeroRoot: the app wrapper, theme host plus overlay outlet.
- ThemeProvider: the theme host alone.
- Row, Col, Center, Spacer, ScrollView: layout primitives.
Every component's props type is exported beside it (ButtonRootProps, DialogRootProps, ToastViewportProps, SelectRootProps, …).
Entry points
import { /* everything below except testing and screen-theme */ } from '@sigx/lynx-zero';
import { expectAnatomy, expectClassGrammar, ForceStates, provideForcedFlags } from '@sigx/lynx-zero/testing';
import { useScreenTheme } from '@sigx/lynx-zero/screen-theme';
Overlays
OverlayHost
<OverlayHost>{/* content */}</OverlayHost>
Provides the overlay registry and renders its content first and the outlet last. ZeroRoot renders one inside a ThemeProvider. Use it directly only when a theme host already exists.
ZeroRootProps
type ZeroRootProps = ThemeProviderProps;
useOverlayPortal
function useOverlayPortal(): OverlayPortal;
interface OverlayPortal {
show(render: () => unknown): void; // mount (or keep) this overlay's content in the outlet
hide(): void; // remove it; also runs on unmount
}
Call in setup. The closure's root must spread OVERLAY_ROOT_STYLE. Without an OverlayHost above, show() warns in development and renders nothing. See custom overlays.
OVERLAY_ROOT_STYLE
const OVERLAY_ROOT_STYLE: { readonly pointerEvents: 'auto' };
The outlet's hit-testing contract. The outlet layer is pointer-events: none and both Lynx engines inherit the value, so every overlay root spreads this into its inline style to take its own touches.
PortalScope
type PortalScopeProps =
& Define.Prop<'setup', () => void, true> // runs in PortalScope's setup: re-provide contexts here
& Define.Prop<'render', () => unknown, true>;
Re-provides contexts inside a portal closure, so components rendered there resolve the injections they would have in place. Pass stable function identities.
useOutletFill
function useOutletFill(): () => Record<string, string | number>;
An absolute style that covers the outlet, which is the whole window. Spread it on an overlay root that must fill the window: the outlet layer is 0×0, so top/right/bottom/left: 0 collapses to nothing. Falls back to the four-edge spelling until the window's size is known. Reactive; read it in the portal closure.
useOverlayInsets
function useOverlayInsets(): () => OverlayInsets;
interface OverlayInsets { top: number; right: number; bottom: number; left: number }
The safe frame's insets inside the window, in px: how far ZeroRoot's box sits inside it. All zero until both are measured, or while the frame pokes out of the window (mid-transition). Reactive.
computeOverlayInsets, containedFrame, fixedOutletRect
function computeOverlayInsets(origin: ElementLayout | null, frame: ElementLayout | null): OverlayInsets;
function containedFrame(origin: ElementLayout | null, frame: ElementLayout | null): ElementLayout | null;
function fixedOutletRect(size: { width: number; height: number } | null, screen: { width: number; height: number }): ElementLayout | null;
The pure functions behind the outlet. containedFrame returns the frame only when it is measured, has a size and lies inside the outlet. computeOverlayInsets is useOverlayInsets over given rects. fixedOutletRect is the outlet's rect: the page root's origin with the layer's size, or the screen's until the layer has one. The outlet is never measured with boundingClientRect.
provideOverlayOrigin
function provideOverlayOrigin(
read: () => ElementLayout | null,
measure?: () => void,
frame?: () => ElementLayout | null,
fullHeight?: () => number,
): void;
Provides the outlet's rect, a re-measure function, the safe frame and the no-keyboard height to the overlays below. OverlayHost calls it. You need it only when writing a host of your own.
Dismiss layers
interface LynxDismissLayer { dismiss(): void }
function registerDismissLayer(layer: LynxDismissLayer): () => void; // returns unregister
function dismissTopLayer(): boolean; // true when a layer took it
function openLayerCount(): number;
function clearDismissLayers(): void; // test seam
The innermost-first stack behind light dismiss. An overlay registers a layer while open and calls dismissTopLayer() from its outside-tap surface, so the innermost overlay closes first. There is no Escape key on Lynx. A back-button handler can call dismissTopLayer() and use the return value.
Anchored positioning
function createAnchorPosition(options?: CreateAnchorPositionOptions): LynxAnchorPosition;
interface CreateAnchorPositionOptions {
placement?: LynxPlacement; // default 'bottom'
offset?: number; // gap to the anchor, px (default 4)
viewportPadding?: number; // min distance from the edge before flipping, px (default 8)
isOpen?: () => boolean; // measure only while open (default: always)
}
interface LynxAnchorPosition {
anchorRef: MainThreadRef<MainThread.Element | null>; // main-thread:ref on the anchor
floatingRef: MainThreadRef<MainThread.Element | null>; // main-thread:ref on the popup
anchorLayoutChange: (event: LayoutChangeEvent) => void; // bindlayoutchange on the anchor
floatingLayoutChange: (event: LayoutChangeEvent) => void; // bindlayoutchange on the popup
track: () => void; // bind to the dismiss surface's touch events
position(): ResolvedPosition | null;
style(): Record<string, string | number>; // the popup's absolute style
}
type LynxPlacement =
| 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end'
| 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end';
Measures the anchor, the popup and the safe frame together, flips to the opposite side when the preferred one overflows, and clamps into the safe frame. Call in setup, and wire both bindings on each element. A navigation transform fires no layout event, so an open popup keeps re-measuring until its anchor holds still, on one shared clock with every other open popup, capped at about two seconds per burst.
The pure placement math is exported too: computeAnchorPosition(anchor, floating, viewport, options?), computeOutletPosition(anchor, floating, origin, screen, options?) and toOutletCoordinates(point, origin). Types: AnchorPositionOptions, ResolvedPosition.
Toasts
createToaster, provideToaster
function createToaster(options?: ToasterOptions): Toaster;
function provideToaster(toaster: Toaster): void;
interface ToasterOptions { exitDuration?: number } // ms a dismissed toast stays for its exit; default 200
interface Toaster {
toasts(): ToastItem[];
show(options: ToastOptions): number;
update(id: number, patch: Partial<ToastOptions>): void;
promise<T>(promise: PromiseLike<T>, options: ToastPromiseOptions<T>): number;
dismiss(id: number): void; // play the exit, then remove
remove(id: number): void; // remove now
}
A toast store you can create headlessly, so a service can toast. Toast.Viewport renders a toaster's toasts, from its toaster prop or from the nearest provideToaster. Types: ToastItem, ToastOptions, ToastInput, ToastPromiseOptions, ToastStatus, ToastPlacement. See Toast.
Axis push-down
interface VariantAxes {
color?: string;
size?: string;
variant?: string;
axes?: Record<string, string | undefined>; // custom axes
mods?: Record<string, boolean | undefined>;
forced?: ForcedFlags; // from an enclosing ForceStates
}
provideVariantAxes
function provideVariantAxes(read: () => VariantAxes): () => VariantAxes;
Provides a carrier's resolved axes to every part below. Pass a reader so prop changes stay reactive. Returns the reader as provided, with any flags an enclosing ForceStates forces, so the carrier stamps its own part from it: const axes = provideVariantAxes(…).
useVariantAxes
function useVariantAxes(): () => VariantAxes;
The nearest provider's axes, or {} outside any carrier.
provideCarriedAxes
function provideCarriedAxes(
anatomy: Anatomy,
part: string,
own: () => Partial<Record<CarriedAxis, string | undefined>>,
): () => VariantAxes;
For a part whose anatomy declares it carries an axis. Call it in the part's setup in place of useVariantAxes(). The part's own value wins; without one, the inherited value passes through. The result is provided to the parts below and returned. Axes the anatomy does not declare for the part are ignored. Throws for an unknown part.
partAxes
function partAxes(a: VariantAxes): Pick<PartBagOptions, 'axes' | 'mods' | 'forced'>;
Flattens axes into partBag options: partBag(anatomy, 'tab', { ...partAxes(axes()), state }).
registerAxisDefaults, resolveVariantAxes
type AxisDefaults = Record<string, string>; // axis → default value
function registerAxisDefaults(byScope: Record<string, AxisDefaults>): void;
function resolveVariantAxes(scope: string, axes: VariantAxes): VariantAxes;
A skin registers its manifest's declared defaults per scope at import: { button: { variant: 'solid', size: 'md', color: 'primary' } }. Re-registering a scope replaces it; different values warn. resolveVariantAxes fills a carrier's unset axes from them (explicit ?? default). A default for an axis other than color / size / variant lands in axes. With nothing registered, the input passes through unchanged.
The part seams
partBag
function partBag(anatomy: Anatomy, part: string, options?: PartBagOptions): LynxPartProps;
interface PartBagOptions {
state?: string;
flags?: Record<string, boolean | undefined>; // disabled, pressed, focus-visible, …
axes?: { color?: string; size?: string; variant?: string; [axis: string]: string | undefined };
mods?: Record<string, boolean | undefined>;
orientation?: 'horizontal' | 'vertical';
placement?: string;
class?: string; // appended last
forced?: ForcedFlags;
}
interface LynxPartProps {
class: string;
'data-scope': string;
'data-part': string;
'data-state'?: string;
'data-orientation'?: string;
'data-placement'?: string;
[attr: string]: unknown;
}
The spreadable props for one rendered part: the zx-* class list and the data-* attributes, from one input, so they cannot disagree. Throws for a part the anatomy does not declare, and where zero's variantAttrs throws (reserved or malformed axis names).
partA11y
function partA11y(options: PartA11yOptions): Record<string, unknown>;
interface PartA11yOptions {
trait?: string;
label?: string;
checked?: boolean;
mixed?: boolean;
selected?: boolean;
expanded?: boolean;
disabled?: boolean;
readonly?: boolean;
}
zero's accessibility guarantees on Lynx's native props: accessibility-element: true, accessibility-trait, accessibility-label and an accessibility-status built from the state (checked, mixed, selected, expanded / collapsed, disabled, read only). Spread it on the node that owns the tap handler.
Press feedback
createPressFeedback
function createPressFeedback(options?: LynxPressFeedbackOptions): LynxPressFeedback;
interface LynxPressFeedbackOptions {
isDisabled?: () => boolean; // no press, either tier, while true
feel?: boolean | LynxPressFeel; // true/omitted: default scale; false: flag only
}
interface LynxPressFeel {
scale?: number; // transform: scale(n) while held; default PRESSED_SCALE, 1 disables it
opacity?: number; // inline opacity while held; unset by default
}
type LynxPressFeedback =
| { pressed(): boolean; readonly mainThread: false; handlers: LynxTier1PressHandlers }
| { pressed(): boolean; readonly mainThread: true; handlers: LynxMainThreadPressHandlers };
Spread handlers on the pressable element and pass pressed() as the part's pressed flag. With the worklet transform, handlers are main-thread touch worklets that scale the element at touch-down (tier 2) and hand the flag to the background. Without it, they are background touch handlers that drive the flag alone. See press feedback. Types: LynxTier1PressHandlers, LynxMainThreadPressHandlers, LynxPressHandlers, MainThreadTouch.
PRESSED_SCALE, PRESSED_OPACITY
const PRESSED_SCALE = 0.97; // the default tier-2 scale
const PRESSED_OPACITY = 0.85; // opt in with feel: { opacity: PRESSED_OPACITY }
Theme engine
ThemeProvider, ThemeProviderProps
The theme host. See ThemeProvider.
useTheme
function useTheme(): ThemeController;
The nearest provider's controller, or themeController outside any provider. Never throws.
themeController
const themeController: ThemeController;
interface ThemeController {
readonly name: ThemeName;
readonly followingSystem: boolean;
readonly fontScale: number;
set(next: ThemeName): void; // pin; stops following the system
toggle(): void; // flip to the pair; an unpaired theme stays
followSystem(): void;
setFontScale(scale: number): void;
}
type ThemeName = string;
The global controller, bound by the root ThemeProvider. Callable from anywhere, with no provider mounted.
makeThemeController, normalizeFontScale
function makeThemeController(state: ThemeState): ThemeController;
function normalizeFontScale(value: unknown, fallback?: number): number;
interface ThemeState { name: ThemeName; following: boolean; fontScale: number }
makeThemeController builds a controller over your own state object; a nested ThemeProvider uses it. normalizeFontScale returns value when it is a finite positive number, else fallback (default 1).
The theme registry
function registerTheme(info: ThemeInfo): void;
function registerThemes(source: ThemeSource): void;
function listThemes(): ThemeInfo[];
function getTheme(name: string): ThemeInfo | undefined;
function pairOf(name: string): string | undefined;
function pickThemeFor(scheme: 'light' | 'dark'): string | undefined;
interface ThemeInfo {
name: string;
colorScheme: 'light' | 'dark';
pair?: string; // what toggle() switches to
swatch?: Record<string, string>; // token → color, for pickers
}
@sigx/zero's theme registry (@sigx/zero/theme/registry), re-exported. It holds theme metadata only; the values are the skin's CSS. A skin seeds it at import. registerThemes seeds it from a design system's token declaration (ThemeSource). pickThemeFor prefers the source's declared default for the scheme, else the first registered theme with that scheme. See theming on the web.
registerTextRamp, textRampVars
function registerTextRamp(values: Record<string, string>): void;
function textRampVars(fontScale: number): Record<string, string>;
A skin registers its unscaled --text-* ramp ({ sm: '14px', … }). textRampVars returns the ramp at a scale as --text-<key> declarations, which the provider sets inline; it is empty at scale 1. Only px values scale.
useScreenTheme
// @sigx/lynx-zero/screen-theme
function useScreenTheme(name: ThemeName): void;
Pins the global theme while a navigation screen is focused, and restores the previous choice on blur. Call it inside a component rendered as a route by @sigx/lynx-navigation, the optional peer.
Layout
Row, Col, Center, Spacer and ScrollView have their own pages. The helpers:
resolveResponsive, Responsive
type Responsive<T> = T | { initial?: T; medium?: T; expanded?: T; large?: T; xlarge?: T };
function resolveResponsive<T>(value: Responsive<T> | undefined, cls: WidthClass): T | undefined;
Resolves a per-breakpoint value against a width class, mobile-first: the value at the first key at or below cls that is defined, else undefined. Read the class with useWidthClass() from @sigx/lynx.
resolveBoxStyle, resolveSpacing
type SpacingValue = number | { x?: number; y?: number; top?: number; right?: number; bottom?: number; left?: number };
interface BoxProps {
width?: Responsive<number | string>;
height?: Responsive<number | string>;
flex?: Responsive<number>;
background?: Responsive<BackgroundValue>;
borderRadius?: Responsive<number>;
padding?: Responsive<SpacingValue>;
margin?: Responsive<SpacingValue>;
}
function resolveBoxStyle(props: BoxProps, cls?: WidthClass): Record<string, unknown>; // cls defaults to 'compact'
function resolveSpacing(value: SpacingValue | undefined, prefix: 'padding' | 'margin'): Record<string, number>;
The style helpers behind the layout primitives. resolveBoxStyle writes flex in long form and resolves background with resolveColorToken. resolveSpacing expands a spacing value into per-side keys (paddingTop, …).
The zero contract (re-exported)
@sigx/lynx-zero re-exports @sigx/zero/contract/core whole, so an app or a part of your own imports the contract from one place. The main groups:
| Group | Exports |
|---|---|
| Class grammar | partClass, stateClass, flagClass, axisClass, modClass, orientationClass, placementClass, layoutClass, themeClass, HOST_CLASS, CLASS_GRAMMAR_VERSION |
| Anatomy | defineAnatomy, defaultPropOf, changeEventOf; types Anatomy, PartSpec, CarriedAxis, ModelSpec, … |
| Attributes | dataAttr, stateAttr, variantAttrs, htmlAttrs, MOD_ATTR_PREFIX, RESERVED_AXES, RESERVED_DATA_ATTRS, VARIANT_AXES |
| Vocabularies | FLAG_VOCABULARY, STATE_VOCABULARY, STATE_NAMES, STATE_SYNONYMS, PLACEMENT_VOCABULARY, SIZE_SCALE_LIST, RECOMMENDED_ROLE_LIST, BASE_SURFACE_TOKEN_LIST |
| Tokens | resolveColorToken, tokenProperty, token, cssVar, defaultSwatch, TOKEN_CATEGORIES, TOKEN_KEY_PATTERN, AXIS_VALUE_PATTERN, ROLE_NAME_PATTERN, TEXT_FIXED_PREFIX, CSS_COLOR_KEYWORDS; types SizeScale, ColorValue, BackgroundValue, … |
| Layout vocabulary | layoutAttrs, layoutAttrSpec, isLayoutValue, parseLayoutAttr, LAYOUT_VOCABULARY, LAYOUT_ATTR_NAMES, LAYOUT_ATTR_PREFIX, BASE_BREAKPOINT_KEY, SPACE_STEPS |
resolveColorToken(value) maps a bare token name ('primary', 'base-100') to var(--color-<name>) and --x to var(--x), and passes literal colors and CSS keywords through. A named CSS color like 'red' resolves as a token name, so write literals as #f00 or rgb().
The Responsive type exported here is the layout primitives' size-class one above, not the contract's.
See the web API reference and the anatomy contract for these in full.
Behaviors (re-exported)
From @sigx/zero/behaviors/core, the platform-neutral behaviors: createControllableState, createId, useIdGenerator, createListController (in registration-order mode on Lynx), moveHighlight, createCollection, defaultItemKey, defaultItemLabel, segmentBy, provideFieldContext, useFieldContext and zeroPlugin, with their types (ControllableState, Collection, FieldContext, ListController, …). See behaviors.
Testing (@sigx/lynx-zero/testing)
expectAnatomy
function expectAnatomy(root: ConformanceNode, anatomy: Anatomy, options?: LynxExpectAnatomyOptions): void;
interface LynxExpectAnatomyOptions extends ExpectAnatomyOptions {
portaled?: readonly string[]; // parts the outlet hosts, re-parented onto their declared parent
}
zero's anatomy oracle over a rendered tree (the container from @sigx/lynx-testing's render()). A stamped data-color / data-size / data-variant on a non-carrier part is checked against its nearest provider: the carrier, or a nearer part that carries the axis. Throws a plain Error.
expectClassGrammar
function expectClassGrammar(root: ConformanceNode, anatomy: Anatomy, options?: { axes?: readonly string[] }): void;
Recomputes each part's zx-* classes from its data-* attributes and asserts the class list matches exactly. Classes outside zx- pass. Throws when no part of the scope rendered.
ForceStates
type ForceStatesProps =
& Define.Prop<'flags', Readonly<Record<string, boolean>>, true> // false forces a flag off
& Define.Prop<'parts', readonly string[], false> // narrow to these parts
& Define.Slot<'default'>;
Forces interaction flags onto every zero part below, for display and screenshots. See forcing states. provideForcedFlags(read) is its runtime half, and ForcedFlags is { flags, parts? }.
ConformanceNode is the structural node shape the oracles read (props, children, parent, _class); @sigx/lynx-testing's nodes satisfy it.
See also
- Usage: the guides behind these exports.
- Components: every component, by category.
@sigx/lynx-zero-legacyAPI: the pre-contract package.
