Tray
A composer-docked panel for questions, approvals, queued messages and pickers.
import { Tray, TrayHeader, TrayTitle and 3 more } 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.
The footer keeps its actions at the end. Actions that don't fit one row wrap under it, still at the end, so in a narrow window the last one (the primary) takes a row of its own and none is pushed past the tray's edge. Its start slot takes a count, a pager or a text button such as Skip: the button's label starts on the tray's text column, under the title, not on its padding.
A notice is a tray of its header alone: a glyph at the text's height, one line, the remedy at its end. Alone, the header is centred in its 40px and its end is a control side, as a footer's.
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.
Picker
The + menu: ListRows in a tray below the composer, labels only; a disabled row says why. Pickers are static, so they open instantly.
rootStyle takes where the tray sits and the knobs below, each a token of its kind.
--tray-bg- PaintDefault var(--bg-elevated)
- Surface fill.
--tray-radius- Ref<Radius>Default var(--radius-frame-tray)
- Corner radius.
--tray-pad- Ref<Space>Default var(--space-1-5)
- Inset around a body of rows (6px). Their corners follow this inset.
--tray-max-h- Ref<Measure>Default min(420px, 50vh)
- Tallest height (420, at most half the viewport); the body scrolls.
--tray-px- Ref<Space> | DerivedDefault calc(var(--tray-pad) + var(--space-1) + (var(--control-xs) - var(--icon-md)) / 2)
- 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 to 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: 120ms rise + fade on mount, 90ms 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- booleanDefault false
- One highlight that glides between the rows (a picker's
+,/or@list); see Glide.
Rest props go to <div> props and render.
divided- booleanDefault false
- Hairline above the footer.
start- ReactNode
- Left-aligned group (a pager, a hint, a text button on the text column); 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- booleanDefault false
label- ReactNode
- The option text (or pass it as children).
description- ReactNode
- Quieter text after the label (text-3).
disabled- booleanDefault false
- Text-4, not selectable.
Rest props go to <button> props and render.