Sheet
A drawer that slides in from an edge — a native dialog, in the top layer.
src/components/rahti_ui/sheet.rs
Basic
A drawer from the right, which is what an unadjusted one is.
shadcn's own demo. `SheetPortal` and `SheetOverlay` are not in it, because both dissolved: a modal `<dialog>` is already in the top layer and cannot be covered, and `::backdrop` is the overlay. A page selecting on `[data-slot=sheet-overlay]` will find nothing.
The trigger is a bare `<button>` wearing `button_variants(…)`. shadcn's has no class list of its own either — its `asChild` was always meant to hand the look to a Button, and a `<button>` inside a `<button>` is not markup.
Watch it close. `dialog.close()` hides the element the instant it is called, so an exit animation would never be seen — the script writes `data-state="closed"`, lets `slide-out-to-right` play, and closes on `animationend`. That is the one genuinely fiddly thing in the file.
Sides
Four edges, and shadcn's four sets of slide utilities.
All four of shadcn's side strings, verbatim — the inset, the width or height, the border on the inner edge, and the matching `slide-in-from-*` and `slide-out-to-*` pair. The side is also written as `data-side`, for a page's own selectors.
The left-hand drawer is the one a Sidebar reaches for on a narrow screen, which is why this component came first.
Without the corner button
A drawer that provides its own way out.
shadcn's `showCloseButton={false}`, spelled `hide_close` — an unwritten Rahti `bool` prop is `false`, so a `close_button` prop would have taken the X off every sheet written. The Separator's `announced` and the Accordion's `instant` are the same trick.
Escape and a backdrop click still work, because both are the browser's rather than the button's. Taking the X off is only reasonable when the drawer carries a dismiss of its own, which this one does.
What the browser does
Most of what Radix's Dialog is for, already in the element.
That list is why the component is small. What is left for the script is opening it, closing it on a backdrop click, holding it on screen for the exit animation, locking the body's scroll, and pointing `aria-labelledby` and `aria-describedby` at the title and description — which needs an id on both halves and so could not be written by either part alone.
The port
What crossed, what dissolved, and what it costs.
What degrades: with no runtime the trigger does nothing and the drawer stays shut. Unlike the Tabs, that is the *correct* resting state rather than a loss — a closed drawer is what a closed drawer should look like, and a `<dialog>` with no `open` attribute is `display: none` by UA rule.
The boundary
The class list without the component.
not-open:hidden max-h-none max-w-none left-auto fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500 backdrop:bg-black/50 backdrop:opacity-0 backdrop:transition-opacity backdrop:duration-300 open:backdrop:opacity-100 inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm
`sheet_content_variants(SheetSide::Right, "")` — the class list a drawer renders with, for a page writing its own `<dialog>`. Write `data-state` alongside it and keep it true: every animation utility in it reads that attribute, and the exit half depends on it still saying `closed` while the element is on screen.
A page taking this route owns the `showModal()` call as well. That is the honest division: the class list is the design, and the design is complete without the component — but a drawer that never opens is not a drawer, and opening it is a method call.