Skip to content
omaris

Containment

Sheet

MD3 side and bottom sheets.

import { Sheet } from 'omaris'
Learn

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 in footer so 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]} with size="full".
  • The modal navigation drawer on a phone: a side="start" sheet with ListItem rows. There is no separate component for it.
  • A panel kept over a live map. modal={false} (no backdrop, page stays tappable), inline so it sits inside the map's box, dismissible={false} and snapPoints. Bind snap so the page can move it. Make the lowest snap a strip showing the one line that matters, like the ETA.

Not for

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 snap when 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 inline sheet's container relative overflow-clip, or the part below the snap point hangs past the edge.
  • Keep handle and drag on 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 persistent modal={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 end sheet. A drawer opens from the start edge.

Quick reference

side
  • start
  • end (default)
  • top
  • bottom
size
  • sm
  • md (default)
  • lg
  • full

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 bindable

Defaults to false

boolean

Bindable.

side

Defaults to 'end'

SheetSide

Edge it hangs off. start/end follow the writing direction.

start
end
top
bottom
size

Defaults to 'md'

SheetSize
sm
md
lg
full
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 bindable

Defaults 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.