npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@solidrt/components

v0.0.51

Published

Readme

@solidrt/components

A collection of components for SolidRT apps, built on the @solidrt/core primitives. Optional: an app can be built with core primitives alone, and a component is just a function returning core elements, so you can always drop down underneath.

LLM agents: see AGENTS.md for a dense, self-contained quickstart.

Installation

bun add @solidrt/components   # peers: @solidrt/core, @solidjs/signals

Per-component prose lives in docs/, one file per module; the props are the typed, commented interfaces in src/ (this package ships its source, so your editor shows them on hover). The README is generated from both.

Theming

Appearance (colors, spacing, border, font roles) comes from one shared, reactive theme backed by a Solid store: reads are tracked, so switching the theme at runtime recolors the live UI without remounting. Two presets ship, darkTheme and lightTheme (default dark); setTheme(preset) switches, setTheme(partial) merges an override one level deep per category. Custom themes are authored with defineTheme.

import { setTheme, darkTheme, lightTheme } from "@solidrt/components"

setTheme(lightTheme)                          // switch to light
setTheme(darkTheme)                           // switch to dark
setTheme({ color: { primary: "#ff2d55" } })   // override one token

Authoring with defineTheme

defineTheme(definition, scheme?) resolves a definition into a theme. Any color may be a single value or a [light, dark] pair; the scheme argument picks the side. Pairs are opt-in per token, and a definition without any needs no scheme at all - modes are a per-theme choice, not a framework requirement (a game ships one look, not two). The built-in presets are one definition resolved twice, so they cannot drift apart.

import { defineTheme, setTheme } from "@solidrt/components"

let def = {
  color: {
    background: ["#ffffff", "#101014"],   // [light, dark]
    primary: "#ff2d55",                   // same in both
    /* ... every color token ... */
  },
  text: { base: 15, ratio: 1.25 },
}

setTheme(defineTheme(def, "dark"))

The type scale derives from text.base (the body size, default 14) and text.ratio (default 1.26): caption, label, body, title, heading sit at base * ratio^(-2..2), rounded to whole pixels, with per-role text.roles overrides for sizes, line heights, and weights.

Tokens

The color tokens are background (window fill), surface (control/card fill), surfaceAlt (subtle raised/track fill), text, textMuted, border, primary/onPrimary, secondary/onSecondary (lower-emphasis accent), danger (validation/destructive), scrim (modal dim), and the feedback pair overlayHover/overlayPressed: translucent tints components draw OVER a control's own fill, so one token pair gives hover/pressed feedback on every fill color, including caller-set ones. Non-color tokens are spacing, radius, borderWidth, and text (the type scale: caption/label/body/title/heading roles, each { size, lineHeight, weight }, plus fontFamily).

Per-component overrides

theme.components restyles a component everywhere without wrapping it: a StyleProps object per component name, merged between the component's themed defaults and each instance's style prop (instance style still wins).

setTheme({ components: { button: { borderRadius: 999 } } })   // pill buttons app-wide

Keys: button, card, badge, switch, checkbox, radio, item, select, segmentedControl, textInput, richTextEditor, tooltip, divider, progressBar, spinner.

Icon slots

theme.icons holds semantic control glyphs as SVG document strings (the same currency as Icon): chevronDown (the Select trigger) and check (the Checkbox mark). Components draw their built-in vector paths by default; a theme that sets a slot swaps that glyph everywhere it appears, and the package still bundles no icon set.

import ChevronDown from "lucide-static/icons/chevron-down.svg"

setTheme({ icons: { chevronDown: ChevronDown } })

API: theme, setTheme, defineTheme, darkTheme, lightTheme, Theme, ThemeDefinition, ThemeColor, ThemedComponent, TextStyle, TextVariant - typed and commented in src/theme.ts.

Policies

Theme answers "how does it look"; policies answer "how does it behave". policy is a second reactive layer derived from the platform facts in @solidrt/core (capabilities, env), so components adapt to touch vs. desktop, window size, and display without every app wiring that logic itself. Reads are reactive like theme: a window resize or the first mouse move on a touch-capable device updates every consuming component live.

The fields:

  • interaction ("touch" | "desktop" | "hybrid") - which affordances a component shows (hover states vs. long-press). Tooltip, Select, and ContextMenu fork on it.
  • density ("comfortable" | "compact" | "dense") - control/hit-target/spacing scale; drives densityScale() (1 / 0.85 / 0.7). A <Density> region overrides it per subtree.
  • motion ("normal" | "reduced" | "none") - animation intensity.
  • focusRing (boolean) - whether focused controls draw a visible focus indicator (true when a keyboard or gamepad/remote is present).
  • textScale (number) - multiplier on type-scale font sizes; defaults to the OS text-scale preference.
  • textWeightDelta (number) - weight compensation (steps of 100) for light-on-dark text on low-DPI displays.
  • navigation ("bottomTabs" | "rail" | "sidebar") - recommended nav layout, derived from the window size class. NavShell follows it.
  • layout ("singlePane" | "twoPane") - recommended pane count, derived from the window size class. SplitView follows it.
setPolicy({ density: "compact" })    // pin a field, overriding the derived value
setPolicy({ density: undefined })    // hand it back to the resolver

setPolicyResolver((caps) => Policies) replaces the whole system-derivation function for full custom control; defaultPolicyResolver is exported to wrap or extend instead of replacing it outright.

API: policy, setPolicy, setPolicyResolver, defaultPolicyResolver, Policies, PolicyResolver, InteractionPolicy, DensityPolicy, MotionPolicy, NavigationPolicy, LayoutPolicy - typed and commented in src/policy.ts.

Layout and style

Most components group their props into two objects, split by one rule: layout properties feed the layout engine (flexbox/grid, sizing, padding, margin, position - the core LayoutProps set) and changing them triggers a relayout; style properties are paint-only and never affect layout: color, backgroundColor, borderColor, borderWidth, borderRadius, opacity, and the transform (x, y, scale, rotate, rotateX/rotateY with perspective, originX/originY, clipRadius). Event handlers (onPointerDown, onKeyDown, ...) are top-level props, never inside layout or style.

StyleProps is that paint set. TextLayoutProps extends LayoutProps with the font fields (fontFamily, fontSize, lineHeight, fontStyle, fontWeight, textAlign, maxLines) because text shaping affects measurement; note lineHeight is a multiplier of fontSize (the theme uses 1.3-1.6), not a pixel value. Option ({ value, label }) is the shared shape of the single-choice controls (Select, SegmentedControl): shared shapes go through this module so components never import a sibling.

API: StyleProps, TextLayoutProps, Option - typed and commented in src/types.ts.

Typography helpers

typeStyle(variant) resolves a theme type-scale role (caption/label/body/title/heading) to font props ready to spread onto a <text> or d-text: fontSize carries policy.textScale, and fontWeight carries the low-DPI weight compensation. Reactive when called inside a tracked scope, like any theme/policy read. Text applies it for you; reach for the helpers when building custom text out of core primitives.

The compensation exists because the renderer rasterizes glyphs unhinted and composites in nonlinear sRGB, which thins light-on-dark text on low-DPI displays as glyphs shrink. typeWeight(weight, size, onDark?) adds policy.textWeightDelta (0 on high-DPI displays) plus one extra step below 16px; dark-on-light text passes through untouched. lightOnDark(text, fill) computes the polarity for a known pair of colors (Button uses it for its fills); omitted, the theme's own palette polarity is used.

API: typeStyle, typeWeight, lightOnDark - typed and commented in src/typography.ts.

Spacing

space(token) is density-scaled spacing: a theme.spacing token (sm/md/lg/xl) multiplied by the density scale of the nearest <Density> region (falling back to the global policy) and rounded to whole pixels. Use it for gaps and paddings that should tighten under compact/dense density; read theme.spacing directly only for distances that must not move with density. Reactive when called inside a tracked scope.

<View layout={{ padding: space("md"), gap: space("sm") }} />

API: space - typed and commented in src/spacing.ts.

Components

Window

The root surface of an app: renders a core <window>, so render() accepts it. Applies layout and style.backgroundColor only (a window cannot be transformed or bordered), plus title and fullscreen.

import { Window } from "@solidrt/components"

function App() {
  return (
    <Window title="My App" style={{ backgroundColor: "#111" }}>
      {/* ... */}
    </Window>
  )
}

API: Window, WindowProps - typed and commented in src/window.tsx.

View

A general-purpose box. Spreads layout onto the underlying view, applies the transform from style, and draws a background and/or border when those style props are set. Takes all pointer event props.

import { View } from "@solidrt/components"

<View
  layout={{ padding: 16, flexDirection: "column", gap: 8 }}
  style={{ backgroundColor: "#222", borderRadius: 8 }}
>
  {/* ... */}
</View>

API: View, ViewProps - typed and commented in src/view.tsx.

Text

Themed text in a layout box. variant picks a typography role from the theme's type scale (caption/label/body/title/heading, default body); color picks a semantic theme color (text, textMuted, primary, onPrimary, danger, default text), with muted as sugar for color="textMuted". Font fields go in layout (they affect measurement) and individually override the role; style.color still wins over color.

Font sizes carry policy.textScale (the OS text-size preference) and weights carry the low-DPI light-on-dark compensation; use the core <text> primitive for text that must not scale.

import { Text } from "@solidrt/components"

<Text variant="title">Section</Text>
<Text muted layout={{ maxLines: 2 }}>Supporting copy that may wrap.</Text>
<Text layout={{ fontSize: 18 }} style={{ color: "#fff" }}>Custom</Text>

Note that lineHeight is a multiplier of fontSize (the theme uses 1.3-1.6), not a pixel value.

API: Text, TextProps, TextColor - typed and commented in src/text.tsx.

Image

Loads and displays an image from a URL or raw bytes (src: string | Uint8Array). URL loads are shared runtime-wide: mounts of the same URL reuse one fetch and one texture, and the bytes are cached on disk (fetched with cache: "force-cache" - no freshness check, so use versioned URLs for content that changes). Concurrent asset fetches are kept polite with a per-host limit; a failed load rejects the mounts sharing it and a later remount retries.

import { Image } from "@solidrt/components"

<Image
  src="https://example.com/avatar.png"
  fallback={PLACEHOLDER_PNG}
  layout={{ width: 64, height: 64 }}
/>

With fit the image fills whatever box layout gives the component - numbers, pct(), or flex - and the fit decides how the pixels map into it (CSS object-fit, centered; "cover" is the ported-web-hero-image answer). Without fit, only numeric layout sizes reach the image; anything else draws at intrinsic size.

<Image src={hero} fit="cover" layout={{ width: pct(100), height: 240 }} />

A failing src is contained by the component: the fallback shows, or the backgroundColor placeholder stays; the error does not propagate to an outer <Errored> boundary. onLoad fires each time a source finishes loading, onError when src fails.

API: Image, ImageProps - typed and commented in src/image.tsx.

SafeArea

Wraps its children in a view padded clear of system UI intrusions (status bars, home indicators, notches). Top and bottom insets are applied by default; pass false to opt out of an edge, or a number to apply the inset with that minimum padding.

import { SafeArea } from "@solidrt/components"

<SafeArea top bottom>...</SafeArea>       // the default edges
<SafeArea bottom={false}>...</SafeArea>   // top only
<SafeArea top={16} bottom={16}>...</SafeArea>  // insets with a 16px minimum
<SafeArea top bottom left right>...</SafeArea> // all four edges

API: SafeArea - typed and commented in src/safe-area.tsx.

TextInput

Text input, single-line by default; multiline wraps at the field's width and edits across lines (Enter inserts a newline, Up/Down move by line; grows with content up to maxRows unless layout.height fixes the box, and scrolls to the caret). Controlled via value/onInput, or uncontrolled via defaultValue; onSubmit fires on Enter (single-line only). Also placeholder, maxLength, autoFocus, disabled, onFocus/onBlur, and hints for IME behavior (keyboard type, capitalization, autocorrect - identifier-like fields want { capitalize: "none", autocorrect: false }).

import { TextInput } from "@solidrt/components"
import { createSignal } from "@solidjs/signals"

function NameField() {
  let [name, setName] = createSignal("")
  return (
    <TextInput
      value={name()}
      onInput={setName}
      onSubmit={(v) => console.log("submitted", v)}
      placeholder="Your name"
      layout={{ width: 240 }}
    />
  )
}

style overrides the themed colors, border, and radius. autoFocus focuses on mount (the on-screen keyboard still waits for a tap).

API: TextInput, TextInputProps - typed and commented in src/text-input.tsx.

RichTextEditor

Edits a rich text Document (styled runs, paragraph attributes) in the same field as TextInput: always multiline, same caret, keys, wrapping, and scrolling. Controlled via value/onInput, or uncontrolled via defaultValue (start from plainDocument("")). Formatting is driven through editorRef, which hands you the document buffer - the component ships no toolbar; the app renders its own controls.

import { RichTextEditor, plainDocument } from "@solidrt/components"

function Notes() {
  let editor
  return (
    <>
      <Button onPress={() => editor.format({ bold: editor.attributes().bold ? null : true })}>B</Button>
      <RichTextEditor
        defaultValue={plainDocument("Start typing...")}
        editorRef={(e) => (editor = e)}
        layout={{ width: 320 }}
        maxRows={10}
      />
    </>
  )
}

Drawn attributes - inline: bold, italic, underline, code (mono), color (a color string), link (a URL string: primary color, underlined); block: heading: 1 | 2 | 3. Other attributes are carried in the document and ignored by the drawing; font-affecting ones feed the text geometry too, so caret and wrap follow the drawn glyphs. Inline atoms (U+FFFC) render as their placeholder character for now.

API: RichTextEditor, RichTextEditorProps - typed and commented in src/rich-text-editor.tsx.

Document model

The value model behind RichTextEditor: a Document is { text, runs, blocks } - plain text plus attributed runs (inline formatting) and per-paragraph blocks. plainDocument(text) builds one from a string; createDocumentBuffer(doc) wraps one in the editing API (format, formatBlock, insertAtom, attributes, selection and edits) that editorRef hands out. ATOM is the inline-atom placeholder character (U+FFFC) for embedded objects.

The shapes (Document, DocumentRun, Attributes, AttributePatch, DocumentBuffer) are exported so an app can build, inspect, persist, and transform documents outside the editor.

API: createDocumentBuffer, plainDocument, ATOM, Document, DocumentRun, DocumentBuffer, DocumentBufferOptions, Attributes, AttributePatch - typed and commented in src/rich-text-document.ts.

ScrollView

A scrollable region; vertical by default, horizontal to flip. Both the wheel and dragging scroll the content: the drag activates after a small movement threshold along the scroll axis, also when it starts on a pressable (the press is cancelled and its feedback retracts), and keeps scrolling when the pointer leaves the box. No momentum/fling yet.

import { ScrollView, Text } from "@solidrt/components"
import { For } from "@solidrt/core"

<ScrollView layout={{ height: 300 }} style={{ backgroundColor: "#111", borderRadius: 8 }}>
  <For each={items()}>{(item) => <Text>{item}</Text>}</For>
</ScrollView>

The underlying geometry primitive createScroll is available from @solidrt/core for building custom scrollers.

API: ScrollView, ScrollViewProps - typed and commented in src/scroll-view.tsx.

Pressable

A pressable box: onPress fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it, and a drag back in restores the pressed state. children and style may each be a function of the live { pressed, hovered, pending } state, so the box restyles on press/hover without extra signals - read the state inside the prop or child expression, never eagerly into a local.

import { Pressable, Text } from "@solidrt/components"

<Pressable
  onPress={() => setCount((c) => c + 1)}
  layout={{ padding: 12 }}
  style={(s) => ({ backgroundColor: s.pressed ? "#333" : "#222", borderRadius: 8 })}
>
  <Text>Tap me</Text>
</Pressable>

disabled takes no pointer events. When pressables nest, the innermost one wins the press. An onPress returning a promise sets pending until it settles; presses meanwhile are ignored, so async actions cannot double-fire.

API: Pressable, PressableProps, PressState - typed and commented in src/pressable.tsx.

Button

A themed press target over Pressable: a padded, centered box with a label. variant picks the visual role - primary (accent fill, the default), secondary, ghost (no fill until hover), danger (destructive) - with fill, hover tint, and label color from the matching theme tokens; no variant draws a border. size (sm/md/lg) pins a minimum width so a row of buttons lines up (a longer label still expands past it); omitted, the button sizes to its content. A string or number child renders as the themed label; any other child renders as-is (an icon, a row, ...).

import { Button } from "@solidrt/components"

<Button onPress={save}>Save</Button>
<Button variant="ghost" onPress={cancel}>Cancel</Button>
<Button variant="danger" size="md" onPress={remove}>Delete</Button>

Press feedback is a slight scale; hover feedback is the theme's overlayHover tint drawn over the fill (non-touch interaction policies only), so it composes with any background, including a caller-set style.backgroundColor. disabled mutes the colors and takes no pointer events.

An onPress returning a promise makes the button an async action: while it is unsettled a centered spinner replaces the label (geometry unchanged, so nothing shifts) and further presses are ignored - a save or submit cannot double-fire.

<Button onPress={async () => { await save() }}>Save</Button>

A focused Button (see createFocusNav) draws a text-colored ring under the focusRing policy - text-colored rather than primary so it stays visible on primary-filled buttons - and activates on Enter, Space, or a remote's center key. focusable (default true) opts out of focus-navigation candidacy; disabled buttons are never candidates.

API: Button, ButtonProps, ButtonVariant - typed and commented in src/button.tsx.

createFocusNav

Focus navigation for pointer-free control (TV remote, keyboard, gamepad), moving real focus across the elements declaring focusable. Two movement types over the same candidates: spatial (arrow keys, dpad) picks the nearest candidate in the pressed direction by on-screen boxes, and sequential (Tab / Shift+Tab) walks visual reading order - rows top to bottom, left to right - wrapping at the ends. Enter / remote center / gamepad south activates the focused control. Nothing is focused until the first navigation press; pointer input works unchanged throughout.

import { createFocusNav } from "@solidrt/components"

function App() {
  let nav = createFocusNav()
  return <window onKeyDown={nav.onKeyDown}>...</window>
}

Attaching nav.onKeyDown on the window is what keeps it cooperative: key events bubble from the focused node, so a focused TextInput keeps its arrow keys and navigation only sees what nothing else consumed. Gamepad dpad/south are wired automatically.

When the focused control disappears (an action replacing it, a screen change), focus lands on the nearest candidate to where it sat as soon as the successor is laid out - the ring follows a Disconnect button into the Connect button that replaces it. A deliberate blur (tapping outside, dismissing the keyboard) stays blurred; the next press resumes at the nearest candidate.

An open Modal traps navigation inside itself with no extra wiring (topmost wins when stacked); pass scope: () => nodeOrNull to trap into some other subtree instead. move/tab/activate are exposed for custom triggers.

API: createFocusNav, FocusNavOptions - typed and commented in src/focus-nav.ts.

Switch

An on/off toggle: the track fills with primary when on and surfaceAlt when off; the thumb slides across. Controlled via value/onChange, or uncontrolled via defaultValue. Built on Pressable, so disabled takes no pointer events. style overrides the track colors and radius.

import { Switch } from "@solidrt/components"
import { createSignal } from "@solidjs/signals"

function NotifyToggle() {
  let [on, setOn] = createSignal(true)
  return <Switch value={on()} onChange={setOn} />
}

API: Switch, SwitchProps - typed and commented in src/switch.tsx.

Checkbox

A checkbox: filled with primary and a drawn checkmark when checked, an empty bordered box otherwise. Controlled via checked/onChange, or uncontrolled via defaultChecked. The mark is the theme.icons.check slot when a theme sets one. style overrides the box colors, border, and radius.

import { Checkbox } from "@solidrt/components"

<Checkbox checked={agree()} onChange={setAgree} />

API: Checkbox, CheckboxProps - typed and commented in src/checkbox.tsx.

RadioGroup / Radio

A single-selection pair: RadioGroup owns the selected value (controlled via value/onChange, or uncontrolled via defaultValue) and shares it with its Radio children; each Radio is a ring with an inner dot when selected. A string/number child of Radio renders as a themed label beside the ring; anything else as-is. disabled on the group disables every option, on a Radio just that one.

import { RadioGroup, Radio } from "@solidrt/components"

<RadioGroup value={plan()} onChange={setPlan}>
  <Radio value="free">Free</Radio>
  <Radio value="pro">Pro</Radio>
  <Radio value="team">Team</Radio>
</RadioGroup>

API: RadioGroup, Radio, RadioGroupProps, RadioProps - typed and commented in src/radio.tsx.

Slider

A horizontal slider: the groove fills up to the thumb, and pressing or dragging the track sets the value from the pointer position. Controlled via value/onChange (fires while dragging), or uncontrolled via defaultValue (defaults to min). min/max default to 0/100; step snaps to an increment, omitted the value is continuous. The drag keeps tracking when the pointer drifts off the track, and an enclosing ScrollView never takes it over.

import { Slider } from "@solidrt/components"

<Slider value={volume()} onChange={setVolume} min={0} max={100} step={1} layout={{ width: 180 }} />

API: Slider, SliderProps - typed and commented in src/slider.tsx.

Card

A themed surface container: a padded column box with a surface fill, a subtle border stroke, and rounded corners, recoloring live on a theme switch. Pass a title for a heading, or lay out the content yourself; override paint via style, spacing/sizing via layout.

import { Card } from "@solidrt/components"

<Card title="Profile" layout={{ width: 360 }}>
  <Text>Card body content.</Text>
</Card>

API: Card, CardProps - typed and commented in src/card.tsx.

Item

A list row: startContent (icon, avatar, checkbox), a label with an optional description under it, and endContent (badge, timestamp, action) pushed to the end. String/number label and description render as themed body and muted caption text; anything else as-is. The dense-data workhorse: rows compose with <For> inside a plain column view or ScrollView - there is no List wrapper, because a column IS the list. Paddings and gaps are density-scaled, so a <Density> region compacts rows wholesale.

import { Item, Badge, Icon } from "@solidrt/components"
import { For } from "@solidrt/core"

<view flexDirection="column">
  <For each={issues()}>
    {(issue) => (
      <Item
        startContent={<Icon src={Bug} />}
        label={issue.title}
        description={issue.assignee}
        endContent={<Badge variant="neutral">{issue.id}</Badge>}
        selected={issue.id === current()}
        onPress={() => setCurrent(issue.id)}
      />
    )}
  </For>
</view>

With onPress the row is interactive: hover/pressed overlay tints (no scale - rows sit flush in a list), focusable for spatial navigation, Enter/remote activation, and a focus ring under the focusRing policy. An async onPress (returning a promise) is not re-fired until it settles. Without onPress the row attaches no press recognizer, so controls inside it (a Switch in a settings row) and enclosing pressables receive pointer events untouched; interactivity is decided at mount. selected fills the row with surfaceAlt; disabled dims the row and takes no pointer events. Separate rows with Divider where needed.

API: Item, ItemProps - typed and commented in src/item.tsx.

Field

A form row: label above the control, the control itself (children, rendered as-is), and a help or error line below. error renders in the danger color and replaces description while set. It draws no chrome and does not reach into the control - error styling of the input itself stays the input's style prop, no hidden magic. The message line only occupies space while there is one; reserve the space with a constant description if the form must not jump when an error appears.

import { Field, TextInput } from "@solidrt/components"

<Field label="Email" description="Used for receipts only." error={emailError()}>
  <TextInput value={email()} onInput={setEmail} />
</Field>

API: Field, FieldProps - typed and commented in src/field.tsx.

Divider

A thin rule in the theme border color. It stretches across its container on the cross axis: full width inside a column, full height inside a row (pass orientation="vertical"). thickness defaults to 1px; add spacing with layout margins, and override the color via style.backgroundColor.

import { Divider } from "@solidrt/components"

<Divider />
<Divider orientation="vertical" />

API: Divider, DividerProps - typed and commented in src/divider.tsx.

Badge

A small rounded pill for counts, labels, and status. variant picks the role: primary (accent fill, the default), neutral (subtle surface), danger. A string/number child renders as the themed label, anything else as-is (an icon, a dot, ...). Override the fill via style.backgroundColor and the label color via style.color.

import { Badge } from "@solidrt/components"

<Badge>New</Badge>
<Badge variant="danger">Error</Badge>

API: Badge, BadgeProps, BadgeVariant - typed and commented in src/badge.tsx.

Spinner

An indeterminate spinner: a 270-degree arc that rotates continuously, driven by core onFrame, so it participates in demand-driven rendering and stops when unmounted. size (diameter, default 24), thickness (default 3), speed (revolutions per second, default 1). Color comes from the theme primary; override via style.color.

import { Spinner } from "@solidrt/components"

<Spinner />
<Spinner size={32} thickness={4} speed={1.5} />

API: Spinner, SpinnerProps - typed and commented in src/spinner.tsx.

ProgressBar

A horizontal progress bar: determinate when given a value in [0, 1] (the fill grows from the left), indeterminate when value is undefined (a short segment slides back and forth, driven by core onFrame). Track is surfaceAlt, fill is primary; override via style.backgroundColor (track) and style.color (fill).

import { ProgressBar } from "@solidrt/components"

<ProgressBar value={0.4} />   // determinate
<ProgressBar />               // indeterminate

API: ProgressBar, ProgressBarProps - typed and commented in src/progress-bar.tsx.

Portal

Renders its child somewhere other than its lexical position: by default at the window root, so overlays (modals, menus, tooltips) escape the clipping and stacking of their surrounding layout; mount targets another node captured from a ref instead. A thin JSX wrapper over core createPortal. The child should be a single element with position="absolute", since it is inserted into the window's flex root. Portals cannot mount during the app's initial render, so gate them behind a signal that starts false.

import { Portal } from "@solidrt/components"

<Show when={open()}>
  <Portal>
    <view position="absolute" right={16} bottom={16}>
      <Card>Saved</Card>
    </view>
  </Portal>
</Show>

API: Portal, PortalProps - typed and commented in src/portal.tsx.

Modal

A centered overlay rendered at the window root via core createPortal: it fills the window with a dimming backdrop (theme scrim; override via backdropColor, "transparent" for no dim) and centers children on top. Control visibility by mounting/unmounting it, e.g. <Show when={open()}>; the gating signal must start false since portals cannot mount during the initial render. Pressing the backdrop calls onClose (unless dismissable is false), pressing the content does not, and while mounted the modal traps createFocusNav inside itself.

import { Modal, Card, Button } from "@solidrt/components"

<Show when={open()}>
  <Modal onClose={() => setOpen(false)}>
    <Card>
      <Button onPress={() => setOpen(false)}>Close</Button>
    </Card>
  </Modal>
</Show>

API: Modal, ModalProps - typed and commented in src/modal.tsx.

Tooltip

A hover-only affordance: under the desktop/hybrid interaction policies, resting a mouse pointer on the wrapped content shows a bubble near it after delay (default 500ms). Under the touch policy it never shows, so tooltip content must stay non-essential. The bubble is portal-mounted at the window root, clamped to the window edges, takes no pointer events, and hides on leave and on press. A string/number content renders as themed body text; anything else as-is. placement picks the side ("top", the default, or "bottom").

import { Tooltip, Button } from "@solidrt/components"

<Tooltip content="Save (Ctrl+S)">
  <Button onPress={save}>Save</Button>
</Tooltip>

API: Tooltip, TooltipProps - typed and commented in src/tooltip.tsx.

Select

A single-choice picker whose presentation forks on the interaction policy: desktop/hybrid opens an anchored dropdown under the trigger (flipping above when there is no room), touch opens a bottom sheet over a scrim. Same contract either way: options is an Option[] ({ value, label }), controlled via value/onChange or uncontrolled via defaultValue; pressing outside closes without a change. placeholder shows in the trigger while nothing is selected. The option list is not scrollable yet, so keep it short. The trigger's chevron is the theme.icons.chevronDown slot when a theme sets one.

import { Select } from "@solidrt/components"

let options = [
  { value: "s", label: "Small" },
  { value: "m", label: "Medium" },
  { value: "l", label: "Large" },
]

<Select options={options} value={size()} onChange={setSize} placeholder="Size" />

API: Select, SelectProps - typed and commented in src/select.tsx.

SegmentedControl

A single-choice row of equal-width segments joined flush: only the control's outermost corners are rounded, hairline dividers separate the segments, and the active segment fills with the theme primary. Hovered segments tint with the theme overlayHover under non-touch interaction policies. options is an Option[]; controlled via value/onChange, or uncontrolled via defaultValue. Override the inactive fill via style.backgroundColor and the outer radius via style.borderRadius.

import { SegmentedControl } from "@solidrt/components"

<SegmentedControl
  options={[{ value: "day", label: "Day" }, { value: "week", label: "Week" }]}
  value={range()}
  onChange={setRange}
/>

API: SegmentedControl, SegmentedControlProps - typed and commented in src/segmented-control.tsx.

ContextMenu

Secondary actions on the wrapped content. The opening gesture follows the physical pointer: right-click for a mouse, long-press (500ms, cancelled by finger travel) for touch. The presentation forks on the interaction policy: touch gets a bottom sheet over a scrim, desktop/hybrid an anchored menu at the pointer that flips up near the bottom edge. items is a ContextMenuItem[] ({ label, onSelect?, disabled? }); pressing outside closes without selecting.

import { ContextMenu } from "@solidrt/components"

<ContextMenu
  items={[
    { label: "Rename", onSelect: rename },
    { label: "Delete", onSelect: remove },
    { label: "Share", disabled: true },
  ]}
>
  <Card>{file.name}</Card>
</ContextMenu>

API: ContextMenu, ContextMenuProps, ContextMenuItem - typed and commented in src/context-menu.tsx.

NavShell

An app shell that arranges primary navigation around the content per policy.navigation: bottom tabs under it (bottomTabs), a narrow rail (rail), or a wide sidebar (sidebar) beside it. The content is a single stable node; switching arrangement only flips the shell's flex direction and remounts the stateless nav strip, so page state survives a resize across a breakpoint. items is a NavItem[] ({ value, label, icon? }; the icon renders above the label in tabs/rail, beside it in the sidebar); controlled via value/onChange, or uncontrolled via defaultValue. Safe areas are the caller's concern: wrap the shell in SafeArea.

import { NavShell, Icon } from "@solidrt/components"

let items = [
  { value: "home", label: "Home", icon: <Icon src={House} /> },
  { value: "settings", label: "Settings", icon: <Icon src={Cog} /> },
]

<NavShell items={items} value={page()} onChange={setPage} layout={{ flex: 1 }}>
  <Show when={page() === "home"} fallback={<Settings />}>
    <Home />
  </Show>
</NavShell>

API: NavShell, NavShellProps, NavItem - typed and commented in src/nav-shell.tsx.

SplitView

A list-detail container driven by policy.layout: twoPane shows the list pane (width listWidth, default 320) beside the detail pane, singlePane shows one at a time per showDetail. Keep pane state (selection, scroll) in the app, not in the panes: crossing a breakpoint re-arranges and can remount them. It draws no chrome and adds no padding; a back affordance in the single-pane detail is the app's to render (fork on policy.layout).

import { SplitView } from "@solidrt/components"

<SplitView
  layout={{ flex: 1 }}
  list={<Inbox onOpen={setSelected} />}
  detail={<Message id={selected()} onBack={() => setSelected(null)} />}
  showDetail={selected() !== null}
/>

API: SplitView, SplitViewProps - typed and commented in src/split-view.tsx.

QrCode

Renders a QR code for data out of primitives: same-color modules in a row collapse into one box, drawn on a light quiet-zone panel; the grid recomputes only when data or level changes. It paints black on white by default (not the theme) so it stays scannable through a theme switch; override color/background only if the contrast still holds. moduleSize (default 6) is pixels per module, margin (default 16) the quiet zone (keep it non-zero), level the error correction (L/M/Q/H, default M: higher tolerates more damage but caps data length sooner), radius the panel's corner radius.

import { QrCode } from "@solidrt/components"

<QrCode data="https://solidjs.com" />
<QrCode data={ticket()} moduleSize={8} level="L" />

API: QrCode, QrCodeProps - typed and commented in src/qrcode.tsx.

Icon

A thin themed wrapper over the core parseSvg primitive. src is a whole SVG document as a string; the component parses it once (memoized), maps the draws to <d-path> in a square viewBox-fitted box (size, default 24) and, for monochrome icons that stroke/fill with currentColor, recolors it via color (default the theme text color). It carries no icon set and no name registry, so any currentColor SVG works (Lucide, Feather, Heroicons, ...) and only the icons you import are bundled. Multi-color documents keep their own fills. For a non-square box, use parseSvg directly.

Icons are just SVG strings: import them as assets (import House from "lucide-static/icons/house.svg", resolved to a string), pull them from a string export, or inline a literal.

import { Icon } from "@solidrt/components"
import House from "lucide-static/icons/house.svg"

<Icon src={House} />
<Icon src={House} size={32} color={theme.color.primary} />

API: Icon, IconProps - typed and commented in src/icon.tsx.

Density

<Density value="compact"> overrides the density policy for its subtree: every density-scaled metric below - space(), control sizes (Checkbox, Switch, Radio, Slider), Item and Button paddings - resolves this value instead of the global policy.density. Regions nest; the nearest wins. Use it to tighten a toolbar, a data table, or a sidebar without per-child props.

import { Density, Item } from "@solidrt/components"

<Density value="dense">
  <For each={rows()}>{(r) => <Item label={r.name} />}</For>
</Density>

densityScale() is the reactive multiplier behind it (1 / 0.85 / 0.7 for comfortable/compact/dense): the nearest <Density> above the calling scope, falling back to policy.density. Call it during component setup or inside JSX/thunks when building custom density-aware components.

API: Density, DensityProps, densityScale - typed and commented in src/density.tsx.

License

MIT. Copyright (c) 2026 Antoine van Wel.