Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Component Props Reference

This page lists every prop for every component in rinch-components. All components use #[derive(Default)] unless noted, meaning Option<T> defaults to None and bool defaults to false.

Keeping this page honest: this file is hand-maintained (prose, worked examples, and “Custom Default” callouts don’t survive mechanical regeneration), so it will drift from the component structs again. Before trusting or editing a prop table, run python3 scripts/audit_component_props_doc.py from the repo root — it cross-references every row here against the real pub struct fields in crates/rinch-components/src/*.rs and prints every type mismatch, missing row, and stray row it finds (see the script’s own docstring for the handful of known non-issues it also reports, e.g. Callback vs a fully-qualified rinch_core::Callback spelling that source uses in a few files — same type, not a bug).

String props: All text/string component props are now String type (not Option<String>). Empty string "" means “not set/use default”. The RSX macro auto-converts string literals: variant: "filled" becomes String::from("filled").

Float literals: Float literals are auto-wrapped: value: 30.0 becomes Some(30.0) for Option<f32> fields.

Universal props: All components support style: and class: in RSX, which are applied to the component’s root DOM element. These support reactive closures {|| expr}. So does any hyphenated attribute — data-nofocus: "", aria-label: "…" — which is written on the root the way it would be on an HTML element; see Attributes on a component.

Both merge with what the component itself put on that root rather than replacing it: class: is added to the component’s own classes, and style: is laid over the component’s own inline declarations, last-wins per property. That matters because a component’s inline style is not decoration — it is how a component publishes a prop to its own stylesheet, and every overlay’s z_index is a custom property written there. style: used to write the whole attribute, which erased all of them (issue #647).

A reactive style: takes its own previous declarations back on each re-run, so it never accumulates: a property the closure has stopped naming is removed, or restored to the value it had been overriding. It overwrites a property it still names where that property stands rather than re-appending it, and it leaves alone one whose current value somebody else has since written. The one case it does not settle is a same-property collision with a style shorthand — see Application Order.

A reactive class: has the same memory: it takes off the words it last wrote before adding the new ones, so it never accumulates its own old values. It does not reach a class it has never named — but naming one is enough to own it. A word the closure emits is the closure’s to remove, so a re-run that stops naming it takes it off and nothing puts it back, and that holds for a class the component itself wrote, its base class included: class: {|| if p.get() { "rinch-checkbox marker" } else { "marker" }} strips rinch-checkbox off the element permanently. That is issue #647’s mechanism and the rule the reactive style: follows too — don’t name a class the component owns.

The merge only holds if the component keeps its side of it. A class: prop is applied to the handle Component::render returned, which is after the component has built its tree and registered its effects. A component effect that then rewrote the whole class attribute — from a base string captured during render — therefore discarded the caller’s class on its first run, and there was no way to get it back short of a re-render. Checkbox { class: "compact", checked_fn: … } styled correctly until the user clicked it once (issue #717). So a component effect adds and removes the one class it is responsible for (NodeHandle::add_class / remove_class) and never writes class wholesale. add_class is idempotent, as DOMTokenList.add is, so an effect that re-runs without changing its answer leaves the attribute alone.

Style shorthands: All elements and components support CSS shorthand props like w, h, m, p, maw, etc. These expand to set_style() calls and compose with component styles. Spacing scale values (xs, sm, md, lg, xl) auto-resolve to var(--rinch-spacing-{value}):

#![allow(unused)]
fn main() {
// Shorthands work on both HTML elements and components
div { p: "md", m: "lg", w: "200px", "Styled div" }
Stack { gap: "md", p: "xl", maw: "600px",
    Text { "Content" }
}
}

See Style Shorthands for the full list.

Prop auto-wrapping: The rsx! macro automatically wraps prop values. See RSX Prop Transformation Rules — do NOT manually wrap in Some(...).

All props are reactive: Every component prop accepts a reactive closure {|| expr} in addition to a static value. When any prop uses a closure, the component automatically re-renders when the signals inside change:

#![allow(unused)]
fn main() {
let active = Signal::new(false);

// Static prop value
Button { variant: "filled", "Always filled" }

// Reactive prop value — re-renders when `active` changes
Button { variant: {|| if active.get() { "filled" } else { "outline" }}, "Toggle" }
}

For more efficient surgical updates (no full re-render), use _fn suffix props where available (e.g., value_fn, checked_fn, opened_fn).

A value_fn write to the text field that currently holds focus is adopted by the field (issue #238): the next keystroke edits the written text, the caret and selection keep their logical position through the rewrite, the write is deferred during an IME composition, and it never commits onchange by itself. See Controlled Input Pattern.


Layout

Stack

Vertical flex container.

PropTypeDefaultDescription
gapString""Spacing between children (xs, sm, md, lg, xl or CSS value)
alignString""CSS align-items (e.g., “center”, “flex-start”)
justifyString""CSS justify-content

Group

Horizontal flex container.

PropTypeDefaultDescription
gapString""Spacing between children (xs, sm, md, lg, xl or CSS value)
alignString""CSS align-items
justifyString""CSS justify-content
wrapboolfalseEnable flex-wrap
growboolfalseChildren flex-grow: 1

SimpleGrid

Auto-layout grid.

PropTypeDefaultDescription
colsOption<u32>NoneNumber of columns (default 1)
min_child_widthString""Min column width for auto-fill; overrides cols
spacingString""Gap between items (xs, sm, md, lg, xl or CSS value)
vertical_spacingString""Vertical gap (xs, sm, md, lg, xl or CSS value); falls back to spacing

Container

Centered max-width wrapper.

PropTypeDefaultDescription
sizeString""Max-width (xs, sm, md, lg, xl)
fluidboolfalseFull width (no max-width)

Center

Centers content horizontally and vertically.

PropTypeDefaultDescription
inlineboolfalseUse inline-flex instead of flex

Space

Empty spacing element.

PropTypeDefaultDescription
wString""Width (spacing scale or CSS value)
hString""Height (spacing scale or CSS value)

Buttons

Button

PropTypeDefaultDescription
variantString""“filled”, “outline”, “light”, “subtle”, “transparent”, “white”, “default”, “gradient”
sizeString""xs, sm, md, lg, xl
colorString""Theme color name
disabledboolfalse
loadingboolfalse
full_widthboolfalse
radiusString""Border radius override
onclickOption<Callback>NoneClick handler

ActionIcon

Icon-only button. For text-based action buttons, use Button with compact styling.

PropTypeDefaultDescription
iconOption<TablerIcon>NoneTabler icon to display
variantString""Same variants as Button
sizeString""xs, sm, md, lg, xl
colorString""Theme color name
radiusString""
disabledboolfalse
loadingboolfalse
onclickOption<Callback>NoneClick handler

CloseButton

Draws a stroked X, sized by icon_size or by size. Until issue #474 it drew nothing at all: the glyph was an empty <span class="rinch-icon rinch-icon--close"> and no stylesheet in the workspace matches that class.

PropTypeDefaultDescription
sizeString""xs, sm, md, lg, xl
radiusString""
disabledboolfalse
icon_sizeOption<u32>NoneThe X’s edge length in px. Unset, it follows size: 12 / 14 / 16 / 20 / 24 for xs…xl
onclickOption<Callback>NoneClick handler

Form Inputs

TextInput

PropTypeDefaultDescription
labelString""
placeholderString""
descriptionString""Help text below input
errorString""Error message; shows error styling
sizeString""xs, sm, md, lg, xl
disabledboolfalse
requiredboolfalse
radiusString""One of xs sm md lg xl, rounding the field. Empty keeps the theme’s default radius
input_typeString""HTML input type (“text”, “email”, etc.)
valueString""Static value
value_fnOption<ReactiveString>NoneReactive value binding (auto-wrapped)
oninputOption<InputCallback>NoneReceives String
onchangeOption<InputCallback>NoneCommit boundary (#226): fires once with the final value when the gesture ends (blur after a modification, or Enter); only if the value changed since focus
onsubmitOption<Callback>NoneFires on Enter key

Textarea

PropTypeDefaultDescription
labelString""
descriptionString""
errorString""
placeholderString""
sizeString""
disabledboolfalse
requiredboolfalse
autosizeboolfalseGrows rows to the controlled value’s \n-line count, clamped to [min_rows, max_rows] (issue #715). Reactive only through value_fn
min_rowsOption<u32>NoneVisible rows; sizes the control to that many lines. Defaults to 2 (HTML default) when unset. A larger CSS min-height wins
max_rowsOption<u32>NoneCaps the control’s height at that many lines via max-height (issue #715, fixed: the control’s rows is a Taffy measure, not a min-height, so the cap can bind). The sheet’s 60–120px size-step min-height still wins when it is taller than the cap — ordinary CSS min over max, not a bug
valueString""
value_fnOption<ReactiveString>NoneReactive value binding (auto-wrapped)
oninputOption<InputCallback>NoneReceives String
onchangeOption<InputCallback>NoneCommit boundary (#226): fires once with the final value when the gesture ends (blur after a modification); only if the value changed since focus

PasswordInput

Custom Default: toggle_visibility defaults to true.

PropTypeDefaultDescription
labelString""
descriptionString""
errorString""
placeholderString""
valueString""
value_fnOption<ReactiveString>NoneReactive value binding (auto-wrapped)
visibleboolfalsePassword visibility state
visible_fnOption<ReactiveBool>NoneReactive visibility (auto-wrapped)
disabledboolfalse
requiredboolfalse
autofocusboolfalse
sizeString""
radiusString""One of xs sm md lg xl, rounding the field. The visibility toggle inside keeps its own radius
toggle_visibilitybooltrueShow/hide the eye toggle button
ontoggleOption<Callback>NoneFires when visibility toggled
oninputOption<InputCallback>NoneReceives String
onchangeOption<InputCallback>NoneCommit boundary (#226): fires once with the final value when the gesture ends (blur after a modification, or Enter); only if the value changed since focus

NumberInput

PropTypeDefaultDescription
labelString""
descriptionString""
errorString""
placeholderString""
valueOption<f64>NoneInitial value; uncontrolled, the steppers move the field from here
value_fnOption<ReactiveString>NoneReactive value binding (auto-wrapped); when set, it is the field’s single write path and the steppers are callback-only
default_valueOption<f64>NoneInitial value of an uncontrolled field when value is absent
minOption<f64>None
maxOption<f64>None
stepOption<f64>None
decimal_scaleOption<u32>NoneNumber of decimal places
prefixString""e.g., “$”
suffixString""e.g., “kg”
disabledboolfalse
hide_controlsboolfalseHide +/- buttons
requiredboolfalse
sizeString""
radiusString""One of xs sm md lg xl. Rounds the field, a prefix/suffix and the steppers together
onincrementOption<Callback>NoneNotification; uncontrolled, the component steps the field itself and reports through oninput (#501)
ondecrementOption<Callback>NoneNotification (see onincrement)
oninputOption<InputCallback>NoneReceives String from direct text entry, and — uncontrolled — each stepper-written value
onchangeOption<InputCallback>NoneCommit boundary (#226): fires once with the final value when the gesture ends (blur after a modification, or Enter); only if the value changed since focus

Checkbox

PropTypeDefaultDescription
labelString""
descriptionString""Secondary line under the label (#707). Puts both in a .rinch-checkbox__body column and aligns the box to the first line
sizeString""
disabledboolfalse
checkedboolfalseStatic checked state
checked_fnOption<ReactiveBool>NoneReactive checked binding (auto-wrapped)
indeterminateboolfalse
onchangeOption<Callback>None

Switch

PropTypeDefaultDescription
labelString""
descriptionString""Secondary line under the label (#707). Puts both in a .rinch-switch__body column and aligns the track to the first line
sizeString""
disabledboolfalse
checkedboolfalse
checked_fnOption<ReactiveBool>NoneReactive checked binding (auto-wrapped)
label_positionString"""start" puts the label before the track; anything else leaves it after
onchangeOption<Callback>None

Select

PropTypeDefaultDescription
labelString""
descriptionString""
errorString""
placeholderString""
sizeString""
disabledboolfalse
requiredboolfalse
valueString""
value_fnOption<ReactiveString>NoneReactive value binding (auto-wrapped)
onchangeOption<InputCallback>NoneReceives selected value as String
dataVec<SelectOption>[]The list of selectable options

Keyboard (#251, #434). The trigger is focusable; Enter or Space opens the list, and while it is open ArrowUp/ArrowDown move the highlight (wrapping), Home/End jump, Enter commits (Space too, unless it is part of a type-ahead prefix), Escape closes without committing, Tab closes, and typing jumps to the first matching label. The trigger keeps the focus throughout, a pointer pick included; focus moving anywhere else closes the list.

Options are passed via data, not as children — the trigger’s _children param is unused. Build a Vec<SelectOption> with SelectOption::new(value, label) (a SelectOption is { value: String, label: String }; an empty label falls back to displaying value):

#![allow(unused)]
fn main() {
Select {
    data: vec![
        SelectOption::new("us", "United States"),
        SelectOption::new("ca", "Canada"),
    ],
    value_fn: move || country.get(),
    onchange: move |v: String| country.set(v),
}
}

The trigger is a Tab stop (tabindex="0", role="combobox", aria-haspopup="listbox", and an aria-expanded that tracks the open state). Enter and Space on it toggle the dropdown; the open list’s keys are above.

Radio / RadioGroup

Radio:

PropTypeDefaultDescription
nameString""Radio group name
valueString""Radio value
labelString""
descriptionString""
checkedboolfalse
checked_fnOption<ReactiveBool>NoneReactive checked binding (auto-wrapped)
disabledboolfalse
sizeString""
colorString""
errorboolfalse
onchangeOption<Callback>None

RadioGroup:

PropTypeDefaultDescription
labelString""
descriptionString""
errorString""
sizeString""Default size for this group’s radios (#707); a Radio’s own size wins, and a radio records its ask as data-size so an explicit "md" is not mistaken for an unset one. A radio that arrives later, or moves in from another group, is sized as it lands (#716)
orientationString""“horizontal” or “vertical”

Slider

PropTypeDefaultDescription
minOption<f64>None
maxOption<f64>None
valueOption<f64>NoneStatic value
value_signalOption<Signal<f64>>NoneDirect signal binding
stepOption<f64>None
sizeString""
colorString""
radiusString""
disabledboolfalse
labelString""Tooltip label format
show_label_on_hoverboolfalse
label_always_onboolfalse
onchangeOption<ValueCallback<f64>>NoneReceives f64

Color

ColorSwatch

A colored square with checkerboard transparency indication.

PropTypeDefaultDescription
colorString""CSS color value
sizeString"28px"Width and height
radiusString"sm"Border radius (xs, sm, md, lg, xl or CSS value)
with_shadowboolfalseShow box shadow
onclickOption<Callback>NoneClick handler

ColorPicker

Interactive color picker with saturation panel, hue/alpha sliders, hex input, and swatches.

PropTypeDefaultDescription
formatString"hex"Output format: hex, hexa, rgb, rgba, hsl, hsla
valueString""Initial color (any parseable CSS color)
value_fnOption<ReactiveString>NoneReactive external value binding
onchangeOption<InputCallback>NoneFires formatted color string on change
alphaboolfalseShow alpha slider
swatchesVec<String>[]Preset swatch colors
swatches_per_rowOption<usize>7Swatches per row
sizeString"md"Size: sm, md, lg, xl
with_inputboolfalseShow hex text input

onchange reports author edits only. A value arriving through value_fn — a peer’s edit, a programmatic set — is adopted silently: the picker applies all four of its internal components (hue, saturation, value, alpha) as one batched transition and fires nothing, because the caller already has that value. Only a drag, a typed hex or a swatch click reports — once per act, with the completed colour, never a partially-applied mixture. This is what lets a consumer bind value_fn to the same store onchange writes without the two chasing each other.

The picker’s state survives its own round trip. An echoing binding — a store that value_fn reads and onchange writes back into — no longer degrades the picker’s hue, saturation, or alpha. Emitted colour strings quantize — to 8-bit RGB under the hex and rgb formats, to whole degrees and percents under hsl — so the echo of the picker’s own emission routinely parses to a slightly different hue/saturation than the internal state it was formatted from (and to no hue at grey, or no alpha under an alpha-less format like hex); the picker recognises such an echo as its own state and leaves the internal signals untouched, so dragging saturation down to grey and back resumes the original hue, and the alpha slider works under the default hex format. A genuinely foreign value (one that denotes a different colour, alpha included) still applies — and even then, channels the value cannot carry are kept rather than fabricated: a grey keeps the current hue (unless it states one, as any hsl() value does — hsl(240, 0%, 50%), the sub-percent hsl(205, 0.3%, 49%), and hsl(0, 0%, 50%) alike — a stated hue is adopted; note that a store which re-spells an RGB grey as hsl() writes hue 0, and the picker adopts it), a black keeps hue and saturation, judged by what the value renders at 8-bit rather than by exact parse floats. One corner is deliberately conceded: under an alpha-dropping display format, an inbound value that restates the picker’s current RGB with an explicitly opaque alpha (rgba(r, g, b, 1)) is indistinguishable from a normalizing store’s echo of the emission and does not apply — bind an alpha-carrying format (hexa, rgba, hsla) when external writes need to drive alpha. More generally, the echo test judges identity at the resolution of the picker’s own emission in the notation the inbound value is written in — 8-bit channels for a hex, named or rgb() value, whole degrees and whole percents for an hsl() value, alpha at two decimals under rgb()/hsl() — never finer than that notation’s serializer writes, whatever finer channels the parser accepts. A difference the emission could not have spelled folds as an echo, in both directions: on an hsl-format wire a stated-hue move of a whole degree applies even at very low chroma, where it does not move the rendered colour by an 8-bit step, while an inbound hsl() value that shares a spelling with the picker’s current colour folds even where 8-bit could tell them apart (a normalizing store re-spelling the picker’s own hex emission as hsl() is indistinguishable from it). A store that re-spells the emission in another notation may also round a tie its own way — #797e81’s hue is exactly 202.5°, which rinch writes as 202 and an exact converter as 203 — so an inbound value compared against what the picker emits (not what it holds) is accepted at either rounding of the emission’s exact value, and at nothing farther: #797e82’s hue is 206⅔°, so a peer’s hsl(206, …) under a hex display still applies. The text field’s write-back guard judges at the same resolution, so field, thumbs and store stay in step.

Accepted colour notations (everywhere a colour string is read — value, value_fn, typed text, swatches): hex in 3, 4, 6, or 8 digits (#rgb, #rgba, #rrggbb, #rrggbbaa, with or without the #), rgb()/rgba() and hsl()/hsla() in both the legacy comma syntax and the modern space-separated syntax (rgb(51 51 102 / 0.5); alpha as a number or a percentage), and the CSS named colours (red, rebeccapurple, transparent). Named colours and function names are case-insensitive, as in CSS. Out-of-range channels clamp to their CSS ranges; hue wraps (#243).

The text field is the author’s while they type. The hex field parses on every keystroke, and a valid prefix of the colour being typed (#336 on the way to #3366cc) is already a parseable colour — it updates the preview and reports through onchange live, but the picker never rewrites the field under the author’s caret while its text denotes the colour the picker already holds. The field text is normalized to the output format when the colour actually moves away from it (a drag, a swatch click, an external value_fn change) — and, since issue #226, when the typed gesture commits (focus leaves the field, or Enter): a committed shorthand like 336 normalizes to #333366, and committed text that parses as no colour reverts to the colour the picker holds, so an attribute-reading consumer never sees a stale shorthand outlive the gesture that typed it. ColorInput’s text field follows the same contract.

“The field’s text” here is the field’s live text, read through NodeHandle::live_value (issue #238) — on the web the .value property the user types into, never the value attribute, which there holds only what was last written programmatically. It is the same read the value_fn binding of TextInput, PasswordInput, Textarea and NumberInput makes before writing, so the echo of a keystroke is not written back there either.

ColorInput

Text input with inline color preview and dropdown ColorPicker.

PropTypeDefaultDescription
labelString""Input label
descriptionString""Description text below the input
errorString""Error message (shows error styling)
placeholderString""Placeholder text
sizeString""Field size: xs, sm, md, lg, xl. Unset or unrecognised means md. Scales the field’s height, padding and font size, and the preview swatch alongside them
radiusString""Field border radius: xs, sm, md, lg, xl. Unset or unrecognised leaves the default sm
disabledboolfalseDisable the input
valueString""Current color value
value_fnOption<ReactiveString>NoneReactive value binding
onchangeOption<InputCallback>NoneFires the formatted color string for every change made in the dropdown picker — a swatch pick, and each frame of a panel or slider drag — and for a typed edit once, at its commit boundary: blur or Enter (#226). Typing previews live in the swatch and the picker but reports only on commit
formatString"hex"Output format
alphaboolfalseShow alpha slider in picker
swatchesVec<String>[]Preset swatch colors
swatches_per_rowOption<usize>7Swatches per row
disallow_inputboolfalseDisallow typing (picker only)
close_on_click_outsidebooltrueDismiss the dropdown when a click lands outside it (#465). Off mounts no backdrop at all, so no click outside the field dismisses it — the pointer half of the pre-#465 behaviour. Escape closes the dropdown either way

size and radius were declared but unread until #263. size always resolves to a step, radius does not: an unrecognised size falls back to md — matching TextInput, whose size parses with unwrap_or_default() — and md reproduces the geometry the field was hard-coded to before, so nothing resizes by upgrading. An unrecognised radius emits no class at all and leaves the base --rinch-radius-sm standing, which is what DropdownMenu, Modal and Card do.

The dropdown is dismissed three ways (#465): clicking the field again, clicking anywhere outside it, and Escape. The outside click is the one that is optional — close_on_click_outside is true by default, matching every other popover in the library, and turning it off mounts no backdrop at all, so the field is the only thing a click dismisses the dropdown with. Escape is not gated by it, so false is the pointer half of the behaviour ColorInput had until #465 rather than a return to it in full. (The prop was declared and read by nothing before that; #263 removed it rather than wire it, because unlike size and radius there was no behaviour behind it to connect, and it came back here with the interaction it names.)

Two details follow from what the trigger is. The field stays clickable while the picker is open — it is a text input, not a button, so a click into it places a caret rather than being swallowed by the dismissal, and it still toggles the dropdown shut. And Escape joins the dismiss stack when the dropdown opens, not when the input mounts, so a ColorInput inside a Modal answers Escape first while its picker is open and leaves the key to the modal the moment it is closed.

The dropdown picker is bound to the input’s current colour (#237). Typed text previews in it live (a parseable keystroke moves its panel and thumbs, at the typed notation’s own grid — a typed hsl(205, 3%, 49%) lands the hue thumb on 205° whatever the display format, and a typed #228be680 moves the alpha thumb), an external value_fn change moves it, and a slider nudge derives from the colour the input currently holds — never from the colour it mounted with. An external arrival is silent all the way through: the picker applies it without reporting, and the input’s onchange fires nothing the caller did not author.

The field shows the colour in the format output spelling. A value or value_fn written in another notation (red, hsl(200, 3%, 49%)) is displayed re-spelled (#ff0000, #797e81 under hex); the field is rewritten only when the colour moves away from its text, so the author’s mid-typing text is left alone as before, and an unparseable commit reverts it to the last committed colour in that same spelling. Under an alpha-dropping format (hex, rgb, hsl) an alpha typed into the field previews in the picker but is dropped at the commit boundary, and an alpha arriving through value_fn is only half-honoured: the dropdown picker adopts it (its alpha thumb moves when alpha is on) and the swatch renders it translucent, but the field cannot spell it and every change the input reports drops it. The corner ColorPicker concedes above applies here too: a later value_fn value restating the colour opaque (#ff0000, rgba(255, 0, 0, 1)) is indistinguishable from the input’s own echo and does not apply, so the picker keeps the alpha — and the swatch snaps opaque on the next reported change while the picker’s alpha thumb does not — until a value carrying a different, non-opaque alpha arrives. Bind hexa/rgba/hsla when alpha must be externally drivable.


Typography

Text

PropTypeDefaultDescription
sizeString""xs, sm, md, lg, xl
weightString""CSS font-weight
colorString""Theme color or “dimmed”
alignString""CSS text-align
inlineboolfalseUse <span> instead of <p>

Title

PropTypeDefaultDescription
orderOption<u8>NoneHeading level 1-6
alignString""CSS text-align
sizeString""Override size independent of order

Code

PropTypeDefaultDescription
blockboolfalseBlock display (<pre>) vs inline (<code>)
colorString""

Kbd

PropTypeDefaultDescription
sizeString""xs, sm, md, lg, xl

Anchor

PropTypeDefaultDescription
hrefString""
targetString""e.g., “_blank”
sizeString""
underlineboolfalse

Blockquote

PropTypeDefaultDescription
citeString""Citation source
iconOption<TablerIcon>None
colorString""
radiusString""

Mark

PropTypeDefaultDescription
colorString""Highlight background color

Highlight

Custom Default: ignore_case defaults to true.

PropTypeDefaultDescription
textString""Full text to display
highlightString""Substring(s) to highlight
colorString""Highlight color
ignore_casebooltrueCase-insensitive matching

Data Display

Avatar

PropTypeDefaultDescription
srcString""Image URL
altString""
nameString""For initials fallback
sizeString""
radiusString""
colorString""
variantString""“filled”, “light”, “outline”

AvatarGroup: spacing: String — overlap spacing.

Badge

PropTypeDefaultDescription
variantString""“filled”, “light”, “outline”, “dot”, “transparent”, “white”, “default”, “gradient”
sizeString""
colorString""
radiusString""One of xs sm md lg xl. A badge is a pill by default, so this squares it off
full_widthboolfalse

Card

PropTypeDefaultDescription
shadowString""xs, sm, md, lg, xl
paddingString""
radiusString""
with_borderboolfalse

CardSection: inherit_padding: bool, with_border: bool.

Paper

PropTypeDefaultDescription
shadowString""xs, sm, md, lg, xl
pString""Padding (spacing scale)
radiusString""
with_borderboolfalse

Divider

PropTypeDefaultDescription
orientationString""“horizontal” or “vertical”
sizeString""
labelString""Text label in the divider
label_positionString""“left”, “center”, “right”

Fieldset

PropTypeDefaultDescription
legendString""
variantString""“default”, “filled”, “unstyled”
sizeString""
disabledboolfalse

Image

PropTypeDefaultDescription
srcString""Image URL
altString""
widthString""CSS width
heightString""CSS height
fitString""CSS object-fit
radiusString""
fallback_srcString""Fallback image URL
captionString""Caption text below image

List / ListItem

List:

PropTypeDefaultDescription
typeString""“ordered” or “unordered”
sizeString""
spacingString""
centerboolfalseCenter items with icons
iconOption<TablerIcon>NoneDefault icon for this list’s items that set none (#707); a ListItem’s own icon wins, and an item that arrives later, or moves in from another list, takes it as it lands (#716)
with_paddingboolfalse

ListItem: icon: Option<TablerIcon> — per-item icon override, which beats List::icon.

A List renders after its items, so its default cannot reach them as a prop: it finds each item that built no icon layout of its own and rebuilds that item into the same markup ListItem uses, moving the content it already held into the content span.

That patch runs at the list’s own render and again on whatever lands beneath the list afterwards — a for reconcile, a show_dom branch, a hand-rolled append_child (issue #716). The second half rides rinch_core::dom::on_child_inserted, a DOM-level hook the four NodeHandle insertion verbs fire: the container registers against its own root and is told, synchronously, each time a subtree arrives. Synchronously matters — the default is in place before the frame that shows the new item is laid out, so nothing flashes.

The same applies to every container default in the library: RadioGroup::size and the four Stepper props, active included. An item moved from one container into another re-resolves against the one it now belongs to, because a container marks the values it supplied as its own (data-list-icon on a list item) and will not touch a value the child asked for itself.

rinch_core::dom::on_child_removed is the other half, and a container takes it only if its per-item answer depends on a sibling’s position (issue #745). Stepper does — a step that goes moves every step behind it backwards, which renumbers it and can restate it — so it registers both (and on either event re-derives only the steps that moved: one it has already settled at the position it still holds costs three attribute reads, issue #748); List and RadioGroup register only the insertion half, since no row going away can change the icon or the size the next one should have. It is fired by the four verbs that take a node out of a tree (remove_child, remove, discard, and replace_with for the node it displaces) and by the implicit detach an insertion verb performs when handed a node that already has a parent, which is what tells the container a child moved away. set_text on an element that has children and set_inner_html replace the whole child list, and fire it as well (issue #1440). A batched DomUpdate::SetText (UpdateBatch::apply) does not.

Because of that, a step’s state, step and disabled are read when the step arrives or moves, not on every change to the list: change them through the StepperStep props (which re-renders the step), not by writing data-state, data-step or disabled on a mounted step by hand or through a reactive root attribute.

The removal half is handed the node the subtree left — its former parent — rather than the node that went, because a removed node is detached and after a discard may have been retired by the backend. A reorder inside one parent fires the insertion half only: the child set did not change.

The rule a container applies to a nested container is the same one its render-time walk applies downwards, read upwards: every registered ancestor is told about an insertion, and each declines a node whose chain up to it crosses one of its own items. So a List inside another list’s item owns its own rows. A removal is tested against the same chain, starting at the parent it left.

Stepper is the one that has to hand something back down. A step’s state decides which icon it draws, and a step draws its icon before its stepper exists — so a step with no state of its own renders the icons for the states it might be moved into and leaves them in its icon box, hidden, for the stepper to promote (issue #709). Promotion re-parents the glyph out of its wrapper before the wrapper goes, since a discard retires the whole subtree (issue #719).

A glyph the step’s props supplied is parked, not discarded, when the step stops drawing it (issue #716). An insertion moves the steps after it, a removal moves them the other way (issue #745), and a keyed for reorder moves a step in either direction, so a step this stepper put into completed can be displaced back out of it — and its icon is a TablerIcon in props that no patch of the rendered tree could rebuild, so it goes back into a hidden wrapper under the content key it serves. A built-in — the tick, or the step number — is rebuildable from nothing and still leaves by discard. Every parked alternate is kept for as long as the stepper owns the step’s state; a step that named its own state is in that state wherever it moves, so for it they are all dropped.

The markers on a step’s icon box are therefore records of the step’s props rather than of what it last drew, which is what lets the pass re-run: data-icon-has names the content keys the box holds a real glyph for, data-icon-live names the key showing, and data-step-derived on the step says this stepper wrote its data-step rather than the caller.


Feedback

Alert

PropTypeDefaultDescription
colorString""
variantString""“filled”, “light”, “outline”, “transparent”, “white”, “default”
titleString""
radiusString""
with_close_buttonboolfalse
iconOption<TablerIcon>None
oncloseOption<Callback>None

Loader

PropTypeDefaultDescription
typeString""“oval”, “bars”, “dots”
sizeString""
colorString""

Progress

PropTypeDefaultDescription
valueOption<f32>NonePercentage 0-100
value_fnOption<ReactiveF32>NoneReactive value binding (auto-wrapped)
colorString""
sizeString""
radiusString""
stripedboolfalse
animatedboolfalse

Skeleton

Custom Default: animate and visible default to true.

PropTypeDefaultDescription
widthString""
heightString""
radiusString""
circleboolfalse
animatebooltrue
visiblebooltrue

Overlays

z_index on an overlay (issue #474). Modal, Drawer, Popover, DropdownMenu and Notification each paint more than one box, and the boxes have to stay ordered against each other: a modal’s panel above its overlay, a menu’s backdrop below its panel. z_index therefore names the level of the component’s base layer — the one whose stylesheet number the per-component table below quotes — and every other layer keeps its existing offset from it. So Modal { z_index: 500 } is overlay 500 / panel 501, and DropdownMenu { z_index: 500 } is panel 500 / backdrop 499; one number moves the component as a piece. Leaving it unset keeps the stylesheet’s own levels.

Every layer that carries a level is a positioned box — fixed for the Modal/Drawer roots, the DropdownMenu backdrop and the Notification; absolute or relative for the panels inside them — so the level orders it within its nearest ancestor stacking context rather than against the whole page. See the stacking-context notes in CLAUDE.md if a raised overlay still sits under something.

One limit worth knowing: the level is published as an inherited custom property, so an overlay of the same type nested inside a raised one inherits its level instead of falling back to the default — Chromium does exactly the same with the same CSS, and before this prop existed both were equal anyway.

A component-level style: prop is safe to combine with it. It used to replace the root’s whole inline style, which dropped the published level and silently returned the overlay to its stylesheet default (issue #647, fixed); a style: now merges into the root’s own declarations the way class: and the style shorthands always have. See the Universal props note at the top of this page.

Dismissing an overlay (issue #474). close_on_escape, and Popover::close_on_click_outside, and Notification::auto_close were declared and read by nothing until the dismissal PR. All three now work, on desktop and on the web, and all three are requests rather than actions: they invoke onclose, and it is the app that closes the overlay by writing the signal opened_fn reads. An overlay with no onclose has nowhere to send the request, so it registers nothing and leaves the key or the click to the app.

Escape goes through the dismiss stack — a last-in-first-out list every open overlay joins while it is mounted. The innermost open overlay answers the key and the ones beneath it do not, so nesting works; an overlay that is mounted but closed passes the key down rather than swallowing it; and a key nothing takes reaches the app unchanged. Some things beat it, all pre-existing and all correct: a document-level set_keyboard_interceptor, which is the app saying it means to have Escape, and an in-progress drag, which Escape cancels — both on either backend — plus, on rinch-web only, a focused RenderSurface, which swallows every key there. An open native <select> popup takes it too, but by joining this same stack rather than by beating it. A custom overlay of your own joins the same stack with push_dismiss_handler — reach for that rather than an interceptor, which is one slot per document and so cannot nest. When to join it is a real choice and these three overlays make the opposite one from ColorInput’s dropdown and the <select> popup; see Register at mount, or at open.

Outside clicks are caught by an invisible full-viewport backdrop the component renders while it is open, one stacking level under its own panel, so a click on the overlay’s own content still reaches the content. Modal and Drawer use their dimming overlay for this and always have; Popover gained one with the same PR. Each such backdrop carries data-backdrop (rinch_core::events::BACKDROP_ATTRIBUTE), so a press of any button outside dismisses, not only a left one. A custom overlay’s own backdrop needs it too, or a right or middle press outside it is swallowed by the backdrop and does nothing (#1093): a data-rid without it runs for a left press only.

auto_close arms a timer when the notification opens and cancels it if the notification closes — or unmounts — first, so a toast the user dismisses by hand does not fire onclose a second time at its original deadline, and one that is shown again gets a fresh delay. 0 is off.

trap_focus contains Tab inside the overlay while it is open: the component stamps data-trap-focus on its root, and both backends cycle Tab and Shift+Tab within that element instead of walking the page. A click outside is untouched — the pointer is not trapped, as a non-modal dialog’s is not.

It also moves focus into the overlay when it opens and gives it back when it closes, because it is the prop that says the overlay is modal. Modal and Drawer focus their first control (or an autofocus one); Popover moves focus only for an autofocus child, which is the HTML popover API’s rule rather than the dialog’s. Closing restores the element that held the keyboard when the overlay opened, provided that element can still take focus, and unmounting while open does the same. A claim the user had already moved outside the overlay is left alone. Details, the nesting rule and the backend timing are in the focus guide.

lock_scroll holds the page still while the overlay is open, and this is the one overlay behaviour that works differently on the two backends. Desktop refuses the scroll gesture: a wheel (which is also what a touch scroll and the MCP scroll tool arrive as) and a scrollbar-thumb press are rejected for any container that is not inside a locking overlay, so the dialog’s own overflow: auto body still scrolls while the page behind does not. Nothing is restyled, so the page does not move and its scroll position is exactly where the user left it. The web sets overflow: hidden on the real <html>, because rinch cannot gate the browser’s own wheel — which also removes the page’s scrollbar, so a web page with a classic (non-overlay) scrollbar shifts sideways when an overlay opens, and a desktop one does not. That is the whole of the divergence.

Two more things follow from “the gesture, not the style” on desktop, and neither has a web equivalent:

  • The page’s scrollbar is still painted while it is locked. You can see it; it will not move. Nothing is drawn differently, only refused.
  • Only input is gated. Programmatic scrolling — NodeHandle::set_scroll_top, an app scrolling a list behind the dialog on purpose — still works. Desktop has no keyboard page-scrolling (PageDown and the arrows scroll nothing at the document level), so there is nothing to gate there.
  • A scrollbar drag already in flight when the lock arrives is ended, not left to keep scrolling the page while the button is held.
  • The native <select> popup is exempt: its option list is appended to <body>, so it is not inside any overlay’s root, and a long list inside a dialog would otherwise be unscrollable. A scroll container of your own that sits outside the overlay and must keep scrolling while it is open (a popup portalled to <body>, a panel that paints over the dialog) says so with data-scroll-lock-exempt; the DOM menu bar’s dropdowns carry it. See Focus.

On the web the lock is the whole page even in island mode — there is one <html> — so a rinch Modal inside an island freezes its host page too.

Locks are counted: two overlays open and the inner one closing leaves the page locked, and an overlay that unmounts while still open releases its own. A custom overlay of your own takes one with node.set_scroll_locked(true) on its root — the node matters, it is the subtree that stays scrollable — and must release it on close and on unmount.

Tooltip

The open state is the class rinch-tooltip--opened on the root, added and removed by the hover effect. .rinch-tooltip__content is display: none and that class is what reveals it, so styling the open tooltip — or overriding how it appears — is a rule of your own against .rinch-tooltip--opened. Spell it with a child combinator, .rinch-tooltip--opened > .rinch-tooltip__content, as the shipped sheet does: the content is a direct child of the root, and a descendant rule also reaches any tooltip nested inside this one’s target. Before issue #760 the class was emitted and matched by nothing, while the reveal was an inline style rewrite on the content node.

disabled wins over opened: Tooltip { opened: true, disabled: true } shows nothing. Before #760 it showed the content, because the inline reveal outranked the sheet’s .rinch-tooltip--disabled rule.

PropTypeDefaultDescription
labelString""Tooltip text
positionString""“top”, “bottom”, “left”, “right”
colorString""
openedboolfalse
disabledboolfalse
with_arrowboolfalse
multilineboolfalse
widthString""

Custom Default: with_overlay, close_on_click_outside, close_on_escape, with_close_button, lock_scroll, trap_focus all default to true.

Positioned with top: var(--rinch-window-top-inset, 0px), so it clears any window chrome rinch draws (the Linux in-app menu bar, the BorderlessWindow titlebar) and is flush with the top of a plain window. See Theming.

PropTypeDefaultDescription
openedboolfalse
opened_fnOption<ReactiveBool>NoneReactive open state (auto-wrapped)
titleString""
sizeString""
radiusString""
with_overlaybooltrue
overlay_opacityOption<f32>NoneBackdrop alpha, 0-1 (default 0.75). Applies to the dimming overlay, not the panel
overlay_blurString""
centeredboolfalse
close_on_click_outsidebooltrueA click on the dimming overlay invokes onclose
close_on_escapebooltrueEscape invokes onclose while this is the innermost open overlay
with_close_buttonbooltrue
paddingString""
z_indexOption<i32>NoneStacking level of the whole modal: the full-viewport overlay sits here and the panel one above it (defaults 200 / 201)
lock_scrollbooltrueHolds the page still while open. Desktop refuses the gesture, web sets overflow: hidden on <html> — see Overlays
trap_focusbooltrueTab cycles inside the overlay while it is open (#474)
oncloseOption<Callback>None

Drawer

Custom Default: with_overlay, close_on_click_outside, close_on_escape, with_close_button, lock_scroll, trap_focus all default to true.

Positioned with top: var(--rinch-window-top-inset, 0px), so it clears any window chrome rinch draws (the Linux in-app menu bar, the BorderlessWindow titlebar) and is flush with the top of a plain window. See Theming.

Opening slides the panel in over 300ms, and closing slides it back out. The closed drawer is visibility: hidden, not display: none (issue #751), which is what lets the slide run: an element that was not being rendered has no before-change style, so a display flip and a transform retarget in one style pass animate nothing — on either backend, since a browser refuses the same shape. See The before-change style, and who has one. Hidden this way the drawer is still excluded from paint — its title and body text included, which desktop drew until issue #829 — from hit testing and from the Tab order. The slide-out keeps the root visible for the 300ms with transition: visibility 0s linear 300ms on the closed state (issues #413, #759), so for those 300ms the closing drawer paints, takes clicks and is reachable by Tab, as it is in a browser; its focus trap is released at the close itself. Reopening part way through cancels the pending hide. An app that wants the old instant close can declare .rinch-drawer__root--hidden { transition: none !important; }.

PropTypeDefaultDescription
openedboolfalse
opened_fnOption<ReactiveBool>NoneReactive open state (auto-wrapped)
titleString""
positionString""“left”, “right”, “top”, “bottom”
sizeString""
with_overlaybooltrue
overlay_opacityOption<f32>NoneBackdrop alpha, 0-1 (default 0.75). Applies to the dimming overlay, not the panel
close_on_click_outsidebooltrueA click on the dimming overlay invokes onclose
close_on_escapebooltrueEscape invokes onclose while this is the innermost open overlay
with_close_buttonbooltrue
paddingString""
z_indexOption<i32>NoneStacking level of the whole drawer: the overlay sits here and the panel one above it (defaults 200 / 201)
lock_scrollbooltrueHolds the page still while open. Desktop refuses the gesture, web sets overflow: hidden on <html> — see Overlays
trap_focusbooltrueTab cycles inside the overlay while it is open (#474)
oncloseOption<Callback>None

Notification

Custom Default: with_close_button defaults to true.

PropTypeDefaultDescription
openedboolfalse
opened_fnOption<ReactiveBool>NoneReactive open state (auto-wrapped)
titleString""
colorString""
positionString""Toast position. The top-* variants offset by --rinch-window-top-inset so they clear window chrome
radiusString""
with_close_buttonbooltrue
with_borderboolfalse
iconOption<TablerIcon>None
auto_closeu320Invoke onclose this many ms after it opens (0 = disabled). Cancelled if it closes or unmounts first
loadingboolfalse
z_indexOption<i32>NoneStacking level of the toast (default 300)
oncloseOption<Callback>None

Popover

Custom Default: close_on_click_outside and close_on_escape default to true.

Popover had no close callback and no reactive open state until #474, so neither dismissal prop had anywhere to send a request. Both onclose and opened_fn were added then, matching DropdownMenu; a Popover without onclose still renders, and simply cannot dismiss itself.

PropTypeDefaultDescription
openedboolfalse
opened_fnOption<ReactiveBool>NoneReactive open state (auto-wrapped)
positionString""
offsetOption<i32>None
radiusString""
shadowString""
with_arrowboolfalse
arrow_sizeOption<f32>None
arrow_offsetOption<f32>None
close_on_click_outsidebooltrueRenders a dismiss backdrop while open; a click on it invokes onclose
close_on_escapebooltrueEscape invokes onclose while this is the innermost open overlay
widthString""
z_indexOption<i32>NoneStacking level of the dropdown; its click-catching backdrop stays one below (defaults 100 / 99)
trap_focusboolfalseTab cycles inside the popover (target + dropdown) while it is open (#474)
oncloseOption<Callback>NoneInvoked when the popover asks to close

Sub-components: PopoverTarget (no props), PopoverDropdown (no props).

ContextMenu

A right-click context menu. No props on the wrapper — state is managed internally.

Sub-components: ContextMenuTarget (no props), ContextMenuDropdown (no props).

Use DropdownMenuItem and DropdownMenuDivider as children of ContextMenuDropdown.

Custom Default: close_on_click_outside and close_on_item_click default to true.

The open state is the class rinch-dropdown-menu--opened on the root, kept in step with opened_fn by an effect. One class reveals both boxes — .rinch-dropdown-menu__dropdown and the outside-click .rinch-dropdown-menu__backdrop are display: none until it lands. Before issue #760 the class matched nothing and each box was revealed by an inline style rewrite. One consequence of the move: “the dropdown” is now named by its class rather than by its position among the children, so a child other than a DropdownMenuDropdown passed after the target is not hidden with the menu. Use the documented pair.

The DropdownMenuDropdown does not have to be a direct child of the menu. rsx! often wraps it on its own — a {Option<NodeHandle>} child, an else if branch, a component with a reactive prop inside an if, a helper component whose body is an if — and a div of your own around it is fine too; the menu opens through any of them. Nesting is handled as well: a closed DropdownMenu inside an open one’s panel stays closed (its panel and its backdrop, which would otherwise take the clicks meant for the outer menu’s items), and an open one inside an open one’s panel — a submenu — shows. The panel’s open rule is a descendant rule that excludes any panel with a closed menu root between it and an open ancestor:

.rinch-dropdown-menu--opened .rinch-dropdown-menu__dropdown:not(
  .rinch-dropdown-menu--opened .rinch-dropdown-menu:not(.rinch-dropdown-menu--opened)
  .rinch-dropdown-menu__dropdown)

Two consequences of that spelling:

  • A nesting limit: an open menu C inside the target (trigger) of a closed menu B that is itself inside an open menu A keeps its panel hidden — B is closed and sits between A and C’s panel, though C’s own root is open. “Inside A” means either half of A, its panel or its target, and the same holds however many closed menus are stacked in triggers between them. (An open menu in a closed menu’s panel is hidden anyway, correctly.) This is a menu in the trigger of a menu in a menu; it opened before #760, and the limit is pinned by a test so it cannot change silently.
  • Do not put a display on a DropdownMenuDropdown. The panel is hidden by a single-class rule, (0,1,0), and shown by the open rule, (0,6,0). A display of your own that beats the hidden rule — a two-class rule such as .my-menu .rinch-dropdown-menu__dropdown { display: flex }, or an inline style: "display: flex" — keeps the panel visible while the menu is closed, so the menu never hides (measured for the two-class rule on desktop and in Chrome, and for the inline style: on desktop). While the menu is open the same declaration loses to the open rule unless it is more specific still. Put the layout (display: flex, a gap) on a child of the DropdownMenuDropdown instead, as with TabsPanel. A style: on the panel that sets anything other than display is kept.
PropTypeDefaultDescription
openedboolfalse
opened_fnOption<ReactiveBool>NoneReactive opened getter — use this for fine-grained updates
on_closeOption<Callback>NoneFired when the user clicks outside the menu or clicks a menu item (with close_on_click_outside/close_on_item_click); the caller should set their opened_fn source to false from here
positionString""
offsetOption<i32>None
radiusString""
shadowString""
close_on_click_outsidebooltrue
close_on_item_clickbooltrue
widthString""
z_indexOption<i32>NoneStacking level of the menu panel; its click-catching backdrop stays one below (defaults 100 / 99)

DropdownMenuTarget, DropdownMenuDropdown, DropdownMenuLabel, DropdownMenuDivider: No props.

DropdownMenuItem:

PropTypeDefaultDescription
left_sectionOption<TablerIcon>None
right_sectionOption<TablerIcon>None
colorString""
disabledboolfalse
onclickOption<Callback>None

HoverCard

PropTypeDefaultDescription
positionString""
offsetOption<i32>None
radiusString""
shadowString""
widthString""
open_delayOption<u32>None
close_delayOption<u32>None
with_arrowboolfalse

Sub-components: HoverCardTarget (no props), HoverCardDropdown (no props).

LoadingOverlay

PropTypeDefaultDescription
visibleboolfalse
overlay_opacityOption<f32>None
overlay_blurString""
loader_typeString""
loader_sizeString""
loader_colorString""
radiusString""
z_indexOption<i32>None
transition_durationOption<u32>None

Tabs

PropTypeDefaultDescription
valueString""Active tab value
default_valueString""
variantString""“default”, “outline”, “pills”
orientationString""“horizontal”, “vertical”
positionString""
growboolfalse
colorString""
radiusString""One of xs sm md lg xl, rounding the tab buttons. outline tabs keep their bottom corners square; the default underline variant has no visible corners to round at rest, though its hover background does

TabsList: grow: bool, justify: String.

Tab:

PropTypeDefaultDescription
valueString""Tab identifier
disabledboolfalse
left_sectionOption<TablerIcon>None
right_sectionOption<TablerIcon>None
onclickOption<Callback>None

TabsPanel: value: String — matches the Tab value.

The active tab and the hidden panel are both stylable. The active tab carries data-active="true" and the class rinch-tabs__tab--active (inactive tabs carry data-active="false"), and the inactive panel carries the HTML hidden attribute, which .rinch-tabs__panel[hidden] turns into display: none. All three are what the shipped sheet styles against, and you can style against them too, with two things to know:

  • Do not put a display on a TabsPanel. hidden hides a panel only through that one rule — desktop has no UA [hidden] rule, and a browser’s loses to any author display — so a display of your own on the panel that wins against .rinch-tabs__panel[hidden] (an inline style: "display: flex" always does) keeps the inactive panel showing, on desktop and in the browser alike. Put the layout on a child of the panel instead.
  • Spell a variant rule from the root with child combinators — .rinch-tabs--pills > .rinch-tabs__list > .rinch-tabs__tab--active, as the shipped sheet does for the active pills and outline styling and the vertical indicator — if your Tabs can nest. A Tabs inside another’s panel is inside that root too, so a descendant rule reaches the nested tabs.

The default variant’s underline is a real element, .rinch-tabs__tab-indicator, appended to each tab button; it carries transition: background-color 150ms ease, so switching tabs animates it. The tab button’s own transition: color starts on a switch as well, but what it animates differs by backend: in a browser the label text inherits the animated colour and fades, while on desktop a transition on an inherited property does not reach descendants (visibility is the one exception, since #759), so the label text — which lives in a child span — snaps to its new colour.

None of these hooks was live before issue #760: the component set neither hook and wrote the active colour, the pills fill, the outline border and the underline inline, so all four of the sheet’s [data-active]/--active rules (eight selectors) were unreachable and the underline’s declared transition never ran. One visible consequence of handing the indicator to the sheet: a vertical default Tabs draws its indicator as a 2px bar down the tab’s right edge, which is what the sheet always declared, where before #760 it drew the horizontal underline along the tab’s bottom edge.

Accordion

PropTypeDefaultDescription
valueString""Active item value
default_valueString""
variantString""“default”, “contained”, “filled”, “separated”
radiusString""
multipleboolfalseAllow multiple open items
chevron_positionString""“left”, “right”
disable_chevron_rotationboolfalseHolds the chevron still while an item is open (#707), through data-disable-chevron-rotation on the root. The rotation is a class rather than an inline style so this can outrank it

AccordionItem: value: String.

AccordionControl: disabled: bool, icon: Option<TablerIcon>, onclick: Option<Callback>.

AccordionPanel: No props.

PropTypeDefaultDescription
separatorString""Custom separator character
separator_marginString""Spacing around separator

BreadcrumbsItem: href: String.

Pagination

Custom Default: total, value, siblings, boundaries default to 1; with_controls defaults to true.

PropTypeDefaultDescription
totalu321Total number of pages
valueu321Current page
siblingsu321Pages visible on each side
boundariesu321Pages at start/end
sizeString""
radiusString""
with_edgesboolfalseShow first/last page buttons
with_controlsbooltrueShow prev/next buttons
colorString""
disabledboolfalse
gapString""
onchangeOption<ValueCallback<u32>>NoneReceives page number
PropTypeDefaultDescription
labelString""
descriptionString""
hrefString""
activeboolfalse
active_fnOption<ReactiveBool>NoneReactive active binding (auto-wrapped)
variantString""“light”, “filled”, “subtle”
colorString""
left_sectionOption<TablerIcon>None
right_sectionOption<TablerIcon>None
disabledboolfalse
children_offsetString""Indentation for nested NavLinks
openedboolfalseNested section expanded
default_openedboolfalse
no_wrapboolfalse
onclickOption<Callback>None

Stepper

PropTypeDefaultDescription
activeu320Active step index. Every step takes its state from its position against this — before it completed, at it in progress, after it inactive (#709) — unless it named a state of its own. A step that arrives later is stated as it lands, and so are the steps it displaced (#716); a step removed, or moved out to another stepper, renumbers and restates the ones behind it too (#745). As a closure (active: {|| sig.get()}) it re-renders the stepper and re-derives
sizeString""
orientationString""“horizontal”, “vertical”
colorString""
radiusString""
icon_sizeString""
allow_next_steps_selectboolfalseMakes each step past active clickable (#707), a step that arrives later included (#716). Grants only: a step’s own allow_step_click / allow_step_select is never taken away. With no on_step_click set, this grants only the class — exactly as before #737
on_step_clickOption<ValueCallback<u32>>NoneFired with a step’s 0-based position when it is clicked or activated (Enter/Space) on a step the stepper considers reachable (#737). Mantine’s default: a strictly completed step (position < active) is clickable without allow_next_steps_select; the active step itself and every step past it need that flag or its own allow_step_click/allow_step_select (Mantine’s active state is 'stepProgress', not 'stepCompleted', so it is not reachable by default either). Stepper does not move active itself — the caller does, from this callback, as Tabs moves its own selection. The index is read from the step’s current position at click time, not baked in when the handler was wired (#714’s pattern), since a later insertion or removal can renumber a step (#716, #745) — and the clickable class is removed the moment a shift makes a step unreachable, unless the step’s own ask keeps it. A step moved into another Stepper calls that stepper’s callback, with its position there (#1428); moved into one with no on_step_click it is no longer wired (no data-rid, tabindex or role), and keeps the clickable class only if it asked for it itself or that stepper’s allow_next_steps_select grants it. A wired step moved out of every stepper keeps its wiring (#1453)
completed_iconOption<TablerIcon>NoneDefault completed icon for the steps that set none (#707); the step’s own wins, and a step that arrives later takes this one as it lands (#716)
progress_iconOption<TablerIcon>NoneDefault in-progress icon for the steps that set none (#707). It stands in for the progress_icon the step did not set, so it outranks that step’s plain icon. A step that arrives later takes it as it lands (#716)

StepperStep:

PropTypeDefaultDescription
labelString""
descriptionString""
iconOption<TablerIcon>NoneDefault icon
completed_iconOption<TablerIcon>NonePer-step override
progress_iconOption<TablerIcon>NonePer-step override
allow_step_clickboolfalseGrants this step the clickable class regardless of its position against active. With Stepper::on_step_click set, also grants the wiring (handler, tabindex="0", data-rid) unless disabled is set (#737)
allow_step_selectboolfalseThe same grant as allow_step_click — Mantine keeps both names, so rinch does too
loadingboolfalse
disabledboolfalseNever clickable, never focusable, whatever else grants it (#737) — the plain HTML disabled attribute, read tag-agnostically
stateString"""completed" or "progress"; anything else is inactive. Leave it unset and the parent Stepper derives it from this step’s position against active (#709). Set it to override that for this one step: what a step names, it keeps
stepOption<u32>NoneStep index, 0-based. Set by the parent Stepper from this step’s position unless named here, in which case that is the index shown — and the number in the icon box, one higher. An index named here changes the label, not the position the state derives from

StepperCompleted: No props.

StepperCompleted is not a position (issue #741): it is what the stepper shows instead of its steps once they are all done. The parent’s step walk skips it, so a StepperStep placed inside it keeps the index and state it rendered itself with, and a whole Stepper nested inside it keeps its own.

It is a skip and not a full stop, so the count runs straight across the block and a step placed after it is numbered like any other. Mantine writes Stepper.Completed last by convention but does not require it: a stepper whose completed block comes first still derives all of its steps.

Tree

Custom Default: level_offset defaults to "md", expand_on_click defaults to true.

PropTypeDefaultDescription
dataVec<TreeNodeData>[]Tree data
treeOption<UseTreeReturn>NoneState from UseTreeReturn::new()
level_offsetString"md"Indentation per level
expand_on_clickbooltrueClick expands/collapses
select_on_clickboolfalseClick selects
render_nodeOption<RenderTreeNode>NoneCustom node renderer
onselectOption<ValueCallback<String>>None
onexpandOption<ValueCallback<String>>None
oncollapseOption<ValueCallback<String>>None
data_sourceOption<Rc<dyn Fn() -> Vec<TreeNodeData>>>NoneReactive data source — when provided, root nodes update when this changes

TreeNodeData: value: String, label: String, children: Vec<TreeNodeData>, disabled: bool, icon: Option<TablerIcon>, payload: Option<Rc<dyn Any>>.


Window

BorderlessWindow

Custom Default: show_minimize, show_maximize, show_close all default to true.

PropTypeDefaultDescription
titleString""Window title in titlebar
radiusString""Corner radius (none, xs, sm, md, lg, xl)
show_minimizebooltrue
show_maximizebooltrue
show_closebooltrue
left_sectionOption<SectionRenderer>NoneCustom titlebar left content
right_sectionOption<SectionRenderer>NoneCustom content before controls
on_minimizeOption<Callback>None
on_maximizeOption<Callback>None
on_closeOption<Callback>None

FloatingPanel

A draggable, resizable floating panel for desktop app patterns — tool panels, property inspectors, floating toolbars. Position and size are controlled via Signal<f32> props (each defaults to an internal signal when not given — x/y default to 50.0, width to 300.0, height to 200.0); the panel writes back to them during drag/resize.

PropTypeDefaultDescription
titleString""Displayed in the panel header; header renders no title span when empty
xOption<Signal<f32>>NoneX position (viewport px)
yOption<Signal<f32>>NoneY position (viewport px)
widthOption<Signal<f32>>NoneWidth (viewport px)
heightOption<Signal<f32>>NoneHeight (viewport px)
min_widthOption<f32>NoneMinimum width constraint (falls back to 200.0)
min_heightOption<f32>NoneMinimum height constraint (falls back to 100.0)
resizablebooltrueWhether the panel can be resized
on_closeOption<Callback>NoneShows a close button in the header when set
#![allow(unused)]
fn main() {
let x = Signal::new(100.0);
let y = Signal::new(100.0);
let w = Signal::new(300.0);
let h = Signal::new(200.0);

rsx! {
    FloatingPanel {
        title: "Properties",
        x: x,
        y: y,
        width: w,
        height: h,
        on_close: move || { /* hide panel */ },
        div { "Panel content here" }
    }
}
}

Callback Types Reference

TypeSignatureUsed By
CallbackRc<dyn Fn()>onclick, onclose, onsubmit, onchange (toggle)
InputCallbackRc<dyn Fn(String)>oninput, onchange (value)
ValueCallback<T>Rc<dyn Fn(T)>Slider (f64), Pagination (u32), Tree (String)
ReactiveBoolRc<dyn Fn() -> bool>checked_fn, active_fn, opened_fn, visible_fn
ReactiveStringRc<dyn Fn() -> String>value_fn
ReactiveF32Rc<dyn Fn() -> f32>Progress value_fn
SectionRendererRc<dyn Fn(&mut RenderScope) -> NodeHandle>BorderlessWindow sections

All callback/reactive props are auto-wrapped by the rsx! macro — just pass closures directly, do not wrap in Some(...) or Rc::new(...).