Sheets

A surface docked to an edge of the screen, holding content secondary to what is behind it. Dock it to the bottom for a bottom sheet, or to a side for a side sheet.

Usage

A sheet is a <dialog>. A modal one takes focus, keeps the page behind it unreachable for as long as it is open, and gives focus back when it closes, all from the browser rather than from script. Clicking the scrim or pressing Escape closes it.

Give the sheet an id and point a trigger at it with command="show-modal" and commandfor. command="close" closes it again. No script and no state of your own, and it works before the page has hydrated.

Bottom sheet

<script lang="ts">
	import { Button, IconButton, Sheet } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
	import Item from '#lib/list/Item.svelte'
</script>

<Button command="show-modal" commandfor="bottom-sheet">Open bottom sheet</Button>
<Sheet id="bottom-sheet" headline="Bottom sheet">
	{#snippet action()}
		<IconButton title="Close" command="close" commandfor="bottom-sheet">
			<Icon>close</Icon>
		</IconButton>
	{/snippet}
	<Item variant="button">Share</Item>
	<Item variant="button">Add to favourites</Item>
	<Item variant="button">Delete</Item>
</Sheet>

Placement

placement picks the edge. bottom and top span the width; start and end run the full height, which is M3's side sheet. The drag handle is a bottom sheet affordance, so it is only drawn there.

Side sheet

Filters, details, or anything secondary to the page behind it.

<script lang="ts">
	import { Button, IconButton, Sheet } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'
</script>

<Button command="show-modal" commandfor="side-sheet">Open side sheet</Button>
<Sheet id="side-sheet" placement="end" headline="Side sheet">
	{#snippet action()}
		<IconButton title="Close" command="close" commandfor="side-sheet">
			<Icon>close</Icon>
		</IconButton>
	{/snippet}
	<p>Filters, details, or anything secondary to the page behind it.</p>
</Sheet>

Standard

modal=false makes a standard sheet: it sits alongside the content and leaves the rest of the page usable, with no scrim and no focus trap. Close it yourself, since there is no light dismiss.

This is the one sheet a trigger cannot open on its own. The invoker commands cover a modal dialog only, so there is no counterpart to show-modal for a standard one: call show() and close() on the component instead. command="close" still closes it.

Standard

The page behind this stays interactive.

<script lang="ts">
	import { Button, IconButton, Sheet } from 'noph-ui'
	import { Icon } from 'noph-ui/icons'

	let standard: ReturnType<typeof Sheet> | undefined = $state()
</script>

<Button onclick={() => standard?.show()}>Open standard sheet</Button>
<Sheet bind:this={standard} id="standard-sheet" modal={false} placement="end" headline="Standard">
	{#snippet action()}
		<IconButton title="Close" command="close" commandfor="standard-sheet">
			<Icon>close</Icon>
		</IconButton>
	{/snippet}
	<p>The page behind this stays interactive.</p>
</Sheet>

Theming

Custom propertyDescription
--np-sheet-container-colorBackground.
--np-sheet-shapeCorner radius on the exposed edges.
--np-sheet-sizeHeight of a bottom or top sheet, width of a side sheet.
--np-sheet-handle-colorThe drag handle.
--np-sheet-elevationShadow.

API

Renders a <dialog> and takes its attributes. bind:element gives you that element, and show() and close() open and close it from outside.

AttributeTypeDefaultDescription
openbooleanfalseBindable. Whether the sheet is showing.
modalbooleantrueBlocks the page behind it and adds a scrim, or sits alongside it.
placement'bottom' | 'top' | 'start' | 'end''bottom'Which edge it docks to.
handlebooleantrueThe drag handle. Drawn on a bottom sheet only.
headlinestring | undefinedundefinedNames the sheet, and is what assistive technology announces.
actionSnippet | undefinedundefinedTrailing action in the header, usually a close button.