Containment
Sheet
MD3 side and bottom sheets.
import { Sheet } from 'omaris' Examples
Sides
Four edges. A bottom sheet is what a phone wants; the sides suit a desktop panel.
<script lang="ts"> import { Button, Sheet } from 'omaris'; let side = $state<'start' | 'end' | 'top' | 'bottom'>('end'); let open = $state(false); function show(next: typeof side) { side = next; open = true; }</script><Button variant="outlined" size="xs" onclick={() => show('start')}>start</Button><Button variant="outlined" size="xs" onclick={() => show('end')}>end</Button><Button variant="outlined" size="xs" onclick={() => show('top')}>top</Button><Button variant="outlined" size="xs" onclick={() => show('bottom')}>bottom</Button><Sheet bind:open {side} title="Filters" description="side = {side}"> <p class="text-body-md text-muted-foreground">Drag the handle to dismiss it, or press outside.</p></Sheet> Snap points
snapPoints are fractions of the screen a bottom sheet settles at, so it can be a peek, a half sheet and a full one without three components.
<script lang="ts"> import { Button, Sheet } from 'omaris'; let open = $state(false); let snap = $state(0);</script><Button variant="tonal" onclick={() => (open = true)}>Open a snapping sheet</Button><Sheet bind:open bind:snap side="bottom" size="full" snapPoints={[0.25, 0.6, 1]} title="Nearby drivers" description="Drag it up and down"> <div class="flex flex-col gap-3"> {#each { length: 10 }, i (i)} <div class="h-12 rounded-shape-md bg-surface-container"></div> {/each} </div></Sheet> Persistent
modal={false} is a panel, not an interruption: no backdrop, and the page behind stays live. With inline it sits inside its own container, so a map keeps its controls and a screen keeps its navigation bar. Bind snap and the page can move it — a focused search box lifts it, a trip drops it.
The page behind: still scrollable, tappable, and where the map would be.
<script lang="ts"> import { Button, Input, List, ListItem, Sheet, Text } from 'omaris'; let snap = $state(0.4); const PLACES = ['Citadel', 'Ankawa', '60m Street', 'Bakhtiari', 'Family Mall', 'Gulan Park'];</script><div class="relative h-96 w-full overflow-clip rounded-shape-lg bg-surface-container"> <div class="grid h-full place-items-start p-4"> <Text variant="body-sm" tone="muted"> The page behind: still scrollable, tappable, and where the map would be. </Text> </div> <Sheet open inline modal={false} dismissible={false} side="bottom" size="full" snapPoints={[0.25, 0.55, 1]} bind:snap classes={{ root: 'border-t border-border' }} > <div class="flex flex-col gap-3"> <Input placeholder="Where to?" onfocus={() => (snap = 1)} /> <div class="flex gap-2"> <Button size="xs" variant="tonal" onclick={() => (snap = 0.25)}>Peek</Button> <Button size="xs" variant="tonal" onclick={() => (snap = 0.55)}>Half</Button> <Button size="xs" variant="tonal" onclick={() => (snap = 1)}>Full</Button> </div> <List> {#each PLACES as place (place)} <ListItem headline={place} supportingText="Tap to go" onclick={() => (snap = 0.25)} /> {/each} </List> </div> </Sheet></div> When to use it
Use it for
- A task that keeps the page in view: filters, editing a customer beside the table, an inspector for the selected row.
side="end", with actions infooterso they stay while the body scrolls. - On a phone, anything that would be a side panel on desktop.
side="bottom", with the drag handle on by default. - A sheet that rests at more than one height, like a map's "Nearby drivers" peek:
snapPoints={[0.25, 0.6, 1]}withsize="full". - The modal navigation drawer on a phone: a
side="start"sheet withListItemrows. There is no separate component for it. - A panel kept over a live map.
modal={false}(no backdrop, page stays tappable),inlineso it sits inside the map's box,dismissible={false}andsnapPoints. Bindsnapso the page can move it. Make the lowest snap a strip showing the one line that matters, like the ETA.
Not for
- A decision that interrupts, like delete or discard → Dialog or
dialog.confirm(). - Feedback → Toast.
- A few actions off a button → Menu.
- A sidebar that is always there → App Shell with a Navigation Drawer.
- A picture full-screen → Image Viewer.
Do
- Choose the edge per viewport:
side={narrow ? 'bottom' : 'end'}. Unlike a Dialog, a sheet does not switch edges on its own. - Put actions in
footer, not at the bottom of the body. A long form scrolls them away. - Bind
snapwhen the page behind reacts, like a map moving its controls above the sheet. Write it when the page knows better: a focused search box lifts the sheet to full, a trip starting drops it to a peek. - Give an
inlinesheet's containerrelative overflow-clip, or the part below the snap point hangs past the edge. - Keep
handleanddragon for a bottom sheet. The handle is also a real close button, so the keyboard gets the same exit.
Don't
- Open a sheet from a sheet. Change what is inside instead.
- Set
dismissible={false}on a modal sheet. A persistentmodal={false}sheet is the one place it is right; there is nothing to dismiss to. - Use a bottom sheet on desktop. Pick
side={wide ? 'end' : 'bottom'}. - Use
side="top"for more than a short filter bar or a notice. The reach on a phone is wrong and the drag is upward. - Put the app's main navigation in an
endsheet. A drawer opens from the start edge.
Quick reference
side startend(default)topbottom
size smmd(default)lgfull
API
Sheet
MD3 side and bottom sheets.
Same native <dialog> foundation as Dialog — focus trap, top layer, Escape — but anchored to an edge and sliding in from it.
It is also draggable, the way a drawer on a phone is: the sheet follows your finger, stops dead at its own edge rather than lifting off it, dims the backdrop as it goes, and either springs back or leaves depending on how far and how fast you threw it. A half-hearted drag always comes back — you have to mean it. The rules for reading a gesture live in drag.ts.
Opening, closing, snapping and dragging are all the same one movement: a distance from the edge, transitioned. There is no entrance keyframe to collide with a gesture, so grabbing the sheet mid-open picks it up from wherever it is, and nothing can re-play an animation it has finished.
A finger drags it from anywhere; a mouse drags it by the handle or the header, so selecting text in the body still works on a desktop.
Every bit of it is keyboard-reachable without dragging: the handle is a real close button, Escape works, and the backdrop is clickable.
modal={false} makes it a panel rather than an interruption: no backdrop, no focus trap, and the page behind stays live — the sheet a map app keeps over the map, resting on a snap point while the map is still dragged and tapped. inline keeps that panel inside its own container instead of the viewport, so it can sit over one region of a screen and leave a navigation bar below it alone. Give that container relative overflow-hidden: a sheet resting on a snap point hangs past the container's edge by the part that is not showing.
import { Sheet } from 'omaris' Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- No description in the source yet.
header- No description in the source yet.
title- No description in the source yet.
description- No description in the source yet.
footer- No description in the source yet.
handle- MD3's drag handle on a bottom sheet — and the grab point.
grip- The visible bar inside that target.
Props
open bindableDefaults to false
boolean Bindable.
side Defaults to 'end'
SheetSide Edge it hangs off. start/end follow the writing direction.
startendtopbottom
size Defaults to 'md'
SheetSize smmdlgfull
title string description string dismissible Defaults to true
boolean Escape, a backdrop press and a full drag close it.
modal Defaults to true
boolean A modal sheet dims and blocks the page. false leaves the page live behind a panel with no backdrop — for a sheet that rests over a map, not one that asks a question.
inline Defaults to false
boolean Keep it inside the nearest positioned ancestor instead of the viewport. Sizes and snap points then read against that box.
handle boolean Show MD3's drag handle. On by default for a bottom sheet.
drag Defaults to true
boolean Follow the pointer. On by default.
dragThreshold Defaults to 0.5
number Fraction of the sheet that has to be dragged away before a release dismisses it. A flick dismisses from anywhere regardless.
snapPoints number[] Resting fractions of the sheet, ascending — [0.4, 1] gives a peek and a full height. Dragging below the smallest one dismisses.
snap bindableDefaults to 1
number Fraction currently rested at. Bindable.
onclose () => void onsnap (snap: number) => void Fires each time the sheet settles on a different snap point.
class string classes SheetClasses Per-part Tailwind overrides. class still covers the root.
headerEnd Snippet Trailing slot in the header — a close button, usually.
children Snippet footer Snippet Pinned action row along the bottom.