Tray
A composer-docked panel for questions, approvals, queued messages and pickers.
import { Tray, TrayHeader, TrayTitle, … } from "@arcadia/ui"Docked to the composer for questions, approvals, queued messages and the +, / and @ pickers. It sits on --bg-elevated with radius 20 and continuous corners (16 round), a soft shadow and the float edge drawn inside; it slides 4px from the composer and never zooms. Give it a name (aria-label, or aria-labelledby on its title) and a role that fits. TrayTitle takes the props of the <span> it renders.
Tray, Dialog or Confirm Dialog? A Tray asks during work and leaves the page usable. Use a Dialog for a task with its own controls, and a Confirm Dialog only to stop something that can't be undone.
A tray that asks something docks to a hailing composer: the horizon holds its light while the product waits on you, and the warm answer button reads as part of that light.
Examples
Multiple choice
kind="multiple" options are checkboxes; the footer's start slot holds a count or a pager.
A confirmation
A neutral question about the page itself, in a tray instead of a dialog, so the work behind it stays in view. It asks nothing of the agent's turn, so the horizon stays dark.
rootStyle takes where the tray sits and the knobs below, each a token of its kind.
| Knob | Takes | Default | Sets |
|---|---|---|---|
Paint | var(--bg-elevated) | Surface fill. | |
Ref<Radius> | var(--radius-frame-tray) | Corner radius. | |
Ref<Space> | var(--space-1-5) | Inset around a body of rows (6px). Their corners follow this inset. | |
Ref<Measure> | min(420px | Tallest height (420, at most half the viewport); the body scrolls. | |
Ref<Space> | Derived | calc(var(--tray-pad) + ${space["--space-1"] | Inline padding of the header, the footer's start and a text body: the rows' first ink (the pad plus a row's text side, 12), so a heading sits on the text it labels (R3). |
--tray-pad sets the row inset. Both TrayOption and ListRow corners follow the tray radius minus that inset.
TrayOption supplies role="radio" or "checkbox" with aria-checked; the surrounding body owns the group role, as above (radiogroup, group). A tray is not modal: focus stays where it was when one arrives, so announce a new question with a live region or move focus to it on purpose.
The keys belong to the flow around the tray. A question flow should handle these:
- 1–9
- Pick an option by its number
- ↓↑
- Move between options
- Space
- Toggle a checkbox option
- Enter
- Submit the answer
Tray
placement- "above" | "below"Default"above"
- Which side of the composer it docks to.
aboverises from below;belowdrops from above. motion- "enter" | "static"Default"enter"
enter: 150ms rise + fade on mount, 100ms fade on close. Only event-driven trays animate (a question or approval arriving, a message queued).static: appears and leaves instantly: hydrated trays and the+,/and@pickers, which open at once.open- boolean
- Omit to mount and unmount the tray yourself (no exit motion). Pass it to let the tray keep itself mounted for its exit fade (
data-exiting,inert) and unmount afterwards. rootStyle- RootStyle<TrayKnobs>
- Typed StyleX override: where the tray sits (layout) and its
--tray-*knobs.
Rest props go to <div> props and render.
No props of its own. Rest props go to <div> props and render.
layout- "rows" | "text"Default"rows"
rows: ListRows / TrayOptions inset 6px.text: prose aligned with the header text.glide- booleanDefaultfalse
- One highlight that glides between the rows (a picker's
+,/or@list); see Glide.
Rest props go to <div> props and render.
divided- booleanDefaultfalse
- Hairline above the footer.
start- ReactNode
- Left-aligned group (a "1 of 2" pager, a hint); the children stay at the end.
Rest props go to <div> props and render.
kind- "single" | "multiple"Default"single"
single: a lettered/numbered badge,role="radio".multiple: a checkbox,role="checkbox".badge- ReactNode
- Badge content for
single("1", "A", or an Icon for "Other"). selected- booleanDefaultfalse
label- ReactNode
- The option text (or pass it as children).
description- ReactNode
- Quieter text after the label (text-3).
disabled- booleanDefaultfalse
- Text-4, not selectable.
Rest props go to <button> props and render.