Lynx/Modules/Zero/API reference
@sigx/lynx-zero · Beta · Component library

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 loading and 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 Toaster store, 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 / invalid once.
  • 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

Every component's props type is exported beside it (ButtonRootProps, DialogRootProps, ToastViewportProps, SelectRootProps, …).

Entry points#

TypeScript
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#

TSX
<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#

TypeScript
type ZeroRootProps = ThemeProviderProps;

useOverlayPortal#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
function useVariantAxes(): () => VariantAxes;

The nearest provider's axes, or {} outside any carrier.

provideCarriedAxes#

TypeScript
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#

TypeScript
function partAxes(a: VariantAxes): Pick<PartBagOptions, 'axes' | 'mods' | 'forced'>;

Flattens axes into partBag options: partBag(anatomy, 'tab', { ...partAxes(axes()), state }).

registerAxisDefaults, resolveVariantAxes#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
function useTheme(): ThemeController;

The nearest provider's controller, or themeController outside any provider. Never throws.

themeController#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
// @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#

TypeScript
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#

TypeScript
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:

GroupExports
Class grammarpartClass, stateClass, flagClass, axisClass, modClass, orientationClass, placementClass, layoutClass, themeClass, HOST_CLASS, CLASS_GRAMMAR_VERSION
AnatomydefineAnatomy, defaultPropOf, changeEventOf; types Anatomy, PartSpec, CarriedAxis, ModelSpec, …
AttributesdataAttr, stateAttr, variantAttrs, htmlAttrs, MOD_ATTR_PREFIX, RESERVED_AXES, RESERVED_DATA_ATTRS, VARIANT_AXES
VocabulariesFLAG_VOCABULARY, STATE_VOCABULARY, STATE_NAMES, STATE_SYNONYMS, PLACEMENT_VOCABULARY, SIZE_SCALE_LIST, RECOMMENDED_ROLE_LIST, BASE_SURFACE_TOKEN_LIST
TokensresolveColorToken, 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 vocabularylayoutAttrs, 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#

TypeScript
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#

TypeScript
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#

TypeScript
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#