# Sheet

Modal panel that slides in from the left or right edge, with focus trap, inert background, nested-overlay coordination, and a footer on the outer frame.

- Package: `@mielui/svelte`
- Component version: `1.1.1`
- Depends on Mielui components: _internal/utils, button, _internal/overlay, typography
- Shared utilities: components/_internal/button-attributes, components/_internal/overlay-close, components/_internal/surface, hugeicons-icon, utils.cn, utils.createContext, transition

## Install

~~~~sh
pnpm dlx @mielui/svelte add sheet
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Close

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| disabled | boolean \| undefined | - | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| variant | ButtonVariant \| undefined | - | No | No | Visual style. Choose it for what the action means, not for decoration. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | - | No | No | Control height. `icon` is a square button for a single icon. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| unstyled | boolean \| undefined | - | No | No | Skip the variant/size base classes and render with `class` alone. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| href | string \| undefined | - | No | No | Renders a link to this address instead of a button. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| surface | "solid" \| "glass" \| undefined | - | No | No | Surface treatment. Omit it to inherit `--mielui-surface` from the theme. |
| allowClickOutside | boolean \| undefined | true | No | No | Closes when the pointer is pressed outside it. Set false to keep it open until it is dismissed another way. |
| side | "left" \| "right" \| undefined | 'right' | No | No | Viewport edge the sheet slides in from. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Description

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Footer

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Header

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| showClose | boolean \| undefined | true | No | No | Shows the close button in the corner. Defaults to true. |



### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| labels | SheetLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| open | boolean \| undefined | false | No | Yes | Whether it is open. Bind it to control the state from outside. |
| onOpenChange | ((open: boolean) => void) \| undefined | - | No | No | Called with the new state whenever it opens or closes. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Title

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Trigger

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| disabled | boolean \| undefined | - | No | No | Prevents activation. A disabled link loses its destination and is skipped by Tab. |
| variant | ButtonVariant \| undefined | - | No | No | Visual style. Choose it for what the action means, not for decoration. |
| size | "sm" \| "md" \| "lg" \| "icon" \| undefined | - | No | No | Control height. `icon` is a square button for a single icon. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: |
| unstyled | boolean \| undefined | - | No | No | Skip the variant/size base classes and render with `class` alone. |
| status | ButtonStatus \| undefined | - | No | No | Controlled visual state. Loading remains focusable and refuses activation. |
| loading | boolean \| undefined | - | No | No | Convenience alias for `status="loading"`. |
| labels | ButtonLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when the button is activated. Loading and disabled buttons do not call it. |
| onkeydown | ((event: KeyboardEvent) => void) \| undefined | - | No | No | Called on key down, before the button handles the key itself. |
| href | string \| undefined | - | No | No | Renders a link to this address instead of a button. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { DefaultProps } from '@mielui/svelte/utils';
import type { Snippet } from 'svelte';
import type { ButtonProps } from '../button';
import Root from './sheet.svelte';
import Close from './sheet-close.svelte';
import Content from './sheet-content.svelte';
import Description from './sheet-description.svelte';
import Footer from './sheet-footer.svelte';
import Header from './sheet-header.svelte';
import Title from './sheet-title.svelte';
import Trigger from './sheet-trigger.svelte';

export type SheetLabels = {
    /** Accessible name of the close button in the header. */
    close?: string;
};

export type SheetProps = {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: SheetLabels;
    /** Whether it is open. Bind it to control the state from outside. */
    open?: boolean;
    /** Called with the new state whenever it opens or closes. */
    onOpenChange?: (open: boolean) => void;
    /** Content rendered inside. */
    children?: Snippet;
};

export type SheetTriggerProps = ButtonProps;
export type SheetTitleProps = DefaultProps;
export type SheetHeaderProps = DefaultProps & {
    /** Shows the close button in the corner. Defaults to true. */
    showClose?: boolean;
};
export type SheetFooterProps = DefaultProps;
export type SheetDescriptionProps = DefaultProps;
export type SheetContentProps = {
    /** Surface treatment. Omit it to inherit `--mielui-surface` from the theme. */
    surface?: 'solid' | 'glass';
    /**
     * Closes when the pointer is pressed outside it. Set false to keep it open until it is
     * dismissed another way.
     */
    allowClickOutside?: boolean;
    /** Viewport edge the sheet slides in from. */
    side?: 'left' | 'right';
} & DefaultProps;

export type SheetCloseProps = ButtonProps;

export type SheetState = {
    open: boolean;
    triggerRef?: HTMLElement | null;
};

export { Close, Content, Description, Footer, Header, Root, Title, Trigger };
~~~~

## Examples

### Glass

~~~~svelte
<script lang="ts">
    import * as Sheet from '@mielui/svelte/components/sheet';
    import { Switch } from '@mielui/svelte/components/switch';

    let compact = $state(false);
    let reduceMotion = $state(false);
</script>

<Sheet.Root>
    <Sheet.Trigger variant="outline">Display settings</Sheet.Trigger>
    <Sheet.Content surface="glass">
        <Sheet.Header>
            <Sheet.Title>Display</Sheet.Title>
            <Sheet.Description>The page stays visible through the panel.</Sheet.Description>
        </Sheet.Header>
        <div class="flex flex-col gap-3">
            <Switch bind:checked={compact} label="Compact rows" />
            <Switch bind:checked={reduceMotion} label="Reduce motion" />
        </div>
        <Sheet.Footer>
            <Sheet.Close>Done</Sheet.Close>
        </Sheet.Footer>
    </Sheet.Content>
</Sheet.Root>
~~~~

### Issue Form

~~~~svelte
<script lang="ts">
    import {
        CircleIcon as Circle,
        AlertCircleIcon as CircleAlert,
        MinusSignIcon as Minus,
        SignalHighIcon as SignalHigh,
        SignalLow01Icon as SignalLow,
        SignalMedium01Icon as SignalMedium,
        Edit01Icon as SquarePen
    } from '@hugeicons/core-free-icons';
    import * as Avatar from '@mielui/svelte/components/avatar';
    import { Button } from '@mielui/svelte/components/button';
    import { Input } from '@mielui/svelte/components/input';
    import Kbd from '@mielui/svelte/components/kbd';
    import { Label } from '@mielui/svelte/components/label';
    import * as Select from '@mielui/svelte/components/select';
    import * as Sheet from '@mielui/svelte/components/sheet';
    import { Textarea } from '@mielui/svelte/components/textarea';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    let open = $state(false);
    let created = $state('');
    let issueTitle = $state('');
    let issueDescription = $state('');
    let status = $state('');
    let priority = $state('');
    let assignee = $state('');

    const statuses = [
        { value: 'todo', label: 'Todo', icon: Circle },
        { value: 'in-progress', label: 'In progress', icon: Circle },
        { value: 'done', label: 'Done', icon: Circle }
    ];

    const priorities = [
        { value: 'none', label: 'No priority', icon: Minus },
        { value: 'urgent', label: 'Urgent', icon: CircleAlert },
        { value: 'high', label: 'High', icon: SignalHigh },
        { value: 'medium', label: 'Medium', icon: SignalMedium },
        { value: 'low', label: 'Low', icon: SignalLow }
    ];

    const assignees = [
        { value: 'an', label: 'mielsense', initials: 'AN' },
        { value: 'sk', label: 'Sam K.', initials: 'SK' },
        { value: 'unassigned', label: 'Unassigned', initials: '?' }
    ];

    const statusMeta = $derived(statuses.find((s) => s.value === status));
    const priorityMeta = $derived(priorities.find((p) => p.value === priority));
    const assigneeMeta = $derived(assignees.find((a) => a.value === assignee));
    const canCreate = $derived(issueTitle.trim().length > 0);

    function reset() {
        issueTitle = '';
        issueDescription = '';
        status = '';
        priority = '';
        assignee = '';
    }

    function createIssue() {
        if (!canCreate) {
            return;
        }
        created = `Created ${issueTitle.trim()} in this preview.`;
        reset();
        open = false;
    }
</script>

<div class="flex flex-col items-center gap-3">
    <div class="flex items-center justify-center">
        <Sheet.Root bind:open>
            <Sheet.Trigger>
                <HugeiconsIcon icon={SquarePen} size={16} />
                New issue
            </Sheet.Trigger>
            <Sheet.Content side="right">
                <Sheet.Header>
                    <div class="flex items-center gap-2.5">
                        <HugeiconsIcon icon={SquarePen} size={18} class="text-foreground-muted" />
                        <Sheet.Title>New issue</Sheet.Title>
                    </div>
                    <Sheet.Description>Create a new issue in Engineering.</Sheet.Description>
                </Sheet.Header>

                <div class="flex flex-col gap-5">
                    <Input
                        bind:value={issueTitle}
                        label="Title"
                        placeholder="Issue title"
                        description="A short, specific summary of the work."
                    />

                    <Textarea
                        bind:value={issueDescription}
                        label="Description"
                        placeholder="Add description…"
                        class="min-h-[120px]"
                    />

                    <div class="flex flex-col gap-3">
                        <div class="flex flex-col gap-1.5">
                            <Label>Status</Label>
                            <Select.Root bind:value={status}>
                                <Select.Trigger aria-label="Status" class="w-full">
                                    <span class="flex min-w-0 items-center gap-2">
                                        {#if statusMeta}
                                            <HugeiconsIcon
                                                icon={statusMeta.icon}
                                                size={14}
                                                class="shrink-0 text-foreground-muted"
                                            />
                                        {/if}
                                        <Select.Value placeholder="Status" />
                                    </span>
                                </Select.Trigger>
                                <Select.Content>
                                    {#each statuses as item (item.value)}
                                        <Select.Item value={item.value} label={item.label}>
                                            <span class="flex items-center gap-2">
                                                <HugeiconsIcon
                                                    icon={item.icon}
                                                    size={14}
                                                    class="text-foreground-muted"
                                                />
                                                {item.label}
                                            </span>
                                        </Select.Item>
                                    {/each}
                                </Select.Content>
                            </Select.Root>
                        </div>

                        <div class="flex flex-col gap-1.5">
                            <Label>Priority</Label>
                            <Select.Root bind:value={priority}>
                                <Select.Trigger aria-label="Priority" class="w-full">
                                    <span class="flex min-w-0 items-center gap-2">
                                        {#if priorityMeta}
                                            <HugeiconsIcon
                                                icon={priorityMeta.icon}
                                                size={14}
                                                class="shrink-0 text-foreground-muted"
                                            />
                                        {/if}
                                        <Select.Value placeholder="Priority" />
                                    </span>
                                </Select.Trigger>
                                <Select.Content>
                                    {#each priorities as item (item.value)}
                                        <Select.Item value={item.value} label={item.label}>
                                            <span class="flex items-center gap-2">
                                                <HugeiconsIcon
                                                    icon={item.icon}
                                                    size={14}
                                                    class="text-foreground-muted"
                                                />
                                                {item.label}
                                            </span>
                                        </Select.Item>
                                    {/each}
                                </Select.Content>
                            </Select.Root>
                        </div>

                        <div class="flex flex-col gap-1.5">
                            <Label>Assignee</Label>
                            <Select.Root bind:value={assignee}>
                                <Select.Trigger aria-label="Assignee" class="w-full">
                                    <span class="flex min-w-0 items-center gap-2">
                                        {#if assigneeMeta}
                                            <Avatar.Root
                                                size="sm"
                                                class="size-5 shrink-0 text-[10px]"
                                            >
                                                <Avatar.Fallback>
                                                    {assigneeMeta.initials}
                                                </Avatar.Fallback>
                                            </Avatar.Root>
                                        {/if}
                                        <Select.Value placeholder="Assignee" />
                                    </span>
                                </Select.Trigger>
                                <Select.Content>
                                    {#each assignees as person (person.value)}
                                        <Select.Item value={person.value} label={person.label}>
                                            <span class="flex items-center gap-2">
                                                <Avatar.Root size="sm" class="size-5 text-[10px]">
                                                    <Avatar.Fallback>
                                                        {person.initials}
                                                    </Avatar.Fallback>
                                                </Avatar.Root>
                                                {person.label}
                                            </span>
                                        </Select.Item>
                                    {/each}
                                </Select.Content>
                            </Select.Root>
                        </div>
                    </div>
                </div>

                <Sheet.Footer>
                    <Sheet.Close onclick={reset}>
                        Cancel
                        <Kbd shortcut="esc" />
                    </Sheet.Close>
                    <Button onclick={createIssue} disabled={!canCreate}>
                        Create issue
                        <Kbd shortcut="enter" />
                    </Button>
                </Sheet.Footer>
            </Sheet.Content>
        </Sheet.Root>
    </div>

    <p role="status" class="text-sm text-foreground-muted">{created}</p>
</div>
~~~~

### Left

~~~~svelte
<script lang="ts">
    import {
        FolderKanbanIcon as FolderKanban,
        Home01Icon as Home,
        InboxIcon as Inbox,
        Menu01Icon as Menu,
        Settings01Icon as Settings
    } from '@hugeicons/core-free-icons';
    import * as Avatar from '@mielui/svelte/components/avatar';
    import { Badge } from '@mielui/svelte/components/badge';
    import { Button } from '@mielui/svelte/components/button';
    import Kbd from '@mielui/svelte/components/kbd';
    import * as Sheet from '@mielui/svelte/components/sheet';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    let open = $state(false);
    let current = $state('home');

    const links = [
        { value: 'home', label: 'Home', icon: Home },
        { value: 'inbox', label: 'Inbox', icon: Inbox, count: 3 },
        { value: 'projects', label: 'Projects', icon: FolderKanban },
        { value: 'settings', label: 'Settings', icon: Settings }
    ];

    function navigate(value: string) {
        current = value;
        open = false;
    }
</script>

<Sheet.Root bind:open>
    <Sheet.Trigger variant="outline">
        <HugeiconsIcon icon={Menu} size={14} />
        Menu
    </Sheet.Trigger>
    <Sheet.Content side="left">
        <Sheet.Header>
            <Sheet.Title>Navigation</Sheet.Title>
            <Sheet.Description>Jump to a section of the app.</Sheet.Description>
        </Sheet.Header>

        <nav aria-label="App sections" class="flex flex-col gap-1">
            {#each links as link (link.value)}
                <Button
                    variant="ghost"
                    class="w-full justify-start gap-2"
                    aria-current={current === link.value ? 'page' : undefined}
                    onclick={() => navigate(link.value)}
                >
                    <HugeiconsIcon icon={link.icon} size={16} />
                    {link.label}
                    {#if link.count}
                        <Badge variant="secondary" class="ml-auto">{link.count}</Badge>
                    {/if}
                </Button>
            {/each}
        </nav>

        <div class="flex items-center gap-3">
            <Avatar.Root size="sm">
                <Avatar.Fallback>AN</Avatar.Fallback>
            </Avatar.Root>
            <div class="flex min-w-0 flex-col">
                <span
                    class="truncate text-sm [font-weight:var(--font-weight-label,500)] text-foreground"
                >
                    mielsense
                </span>
                <span class="truncate text-xs text-foreground-muted">mielsense@mielui.ui</span>
            </div>
        </div>

        <Sheet.Footer>
            <Sheet.Close>
                Close
                <Kbd shortcut="esc" />
            </Sheet.Close>
        </Sheet.Footer>
    </Sheet.Content>
</Sheet.Root>
~~~~

### Right

~~~~svelte
<script lang="ts">
    import { SlidersHorizontalIcon as SlidersHorizontal } from '@hugeicons/core-free-icons';
    import { numberShuffle } from '@mielui/svelte/actions/number-shuffle';
    import { Badge } from '@mielui/svelte/components/badge';
    import { Button } from '@mielui/svelte/components/button';
    import { Checkbox } from '@mielui/svelte/components/checkbox';
    import Kbd from '@mielui/svelte/components/kbd';
    import { Label } from '@mielui/svelte/components/label';
    import * as Select from '@mielui/svelte/components/select';
    import * as Sheet from '@mielui/svelte/components/sheet';
    import { Switch } from '@mielui/svelte/components/switch';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    let open = $state(false);
    let status = $state('all');
    let onlyMine = $state(false);
    let includeArchived = $state(false);
    let hasAssignee = $state(true);
    let hasLabels = $state(true);

    const statuses = [
        { value: 'all', label: 'All statuses' },
        { value: 'open', label: 'Open' },
        { value: 'closed', label: 'Closed' }
    ];

    const activeCount = $derived(
        (status !== 'all' ? 1 : 0) +
            (onlyMine ? 1 : 0) +
            (includeArchived ? 1 : 0) +
            (!hasAssignee ? 1 : 0) +
            (!hasLabels ? 1 : 0)
    );

    function reset() {
        status = 'all';
        onlyMine = false;
        includeArchived = false;
        hasAssignee = true;
        hasLabels = true;
    }

    function apply() {
        open = false;
    }
</script>

<Sheet.Root bind:open>
    <Sheet.Trigger variant="outline">
        <HugeiconsIcon icon={SlidersHorizontal} size={14} />
        Filters
        {#if activeCount > 0}
            <Badge variant="secondary">
                <span use:numberShuffle={{ value: activeCount }}>{activeCount}</span>
            </Badge>
        {/if}
    </Sheet.Trigger>
    <Sheet.Content side="right">
        <Sheet.Header>
            <Sheet.Title>Filters</Sheet.Title>
            <Sheet.Description>Narrow the issue list.</Sheet.Description>
        </Sheet.Header>

        <div class="flex flex-col gap-5">
            <div class="flex flex-col gap-1.5">
                <Label>Status</Label>
                <Select.Root bind:value={status}>
                    <Select.Trigger aria-label="Status" class="w-full" />
                    <Select.Content>
                        {#each statuses as item (item.value)}
                            <Select.Item value={item.value}>{item.label}</Select.Item>
                        {/each}
                    </Select.Content>
                </Select.Root>
            </div>

            <div class="flex flex-col gap-3">
                <Switch bind:checked={onlyMine} label="Only my issues" />
                <Switch bind:checked={includeArchived} label="Include archived" />
            </div>

            <fieldset class="flex flex-col gap-3">
                <legend
                    class="pb-3 text-sm [font-weight:var(--font-weight-label,500)] text-foreground"
                >
                    Fields
                </legend>
                <Checkbox bind:checked={hasAssignee} label="Has assignee" />
                <Checkbox bind:checked={hasLabels} label="Has labels" />
            </fieldset>
        </div>

        <Sheet.Footer>
            <Sheet.Close>
                Close
                <Kbd shortcut="esc" />
            </Sheet.Close>
            <Button variant="ghost" disabled={activeCount === 0} onclick={reset}>Reset</Button>
            <Button onclick={apply}>
                Apply filters
                {#if activeCount > 0}
                    <Badge variant="secondary">
                        <span use:numberShuffle={{ value: activeCount }}>{activeCount}</span>
                    </Badge>
                {/if}
                <Kbd shortcut="enter" />
            </Button>
        </Sheet.Footer>
    </Sheet.Content>
</Sheet.Root>
~~~~

### Stay Open

~~~~svelte
<script lang="ts">
    import { Input } from '@mielui/svelte/components/input';
    import * as Sheet from '@mielui/svelte/components/sheet';
</script>

<Sheet.Root>
    <Sheet.Trigger variant="outline">Edit profile</Sheet.Trigger>
    <Sheet.Content allowClickOutside={false}>
        <Sheet.Header showClose={false}>
            <Sheet.Title>Edit profile</Sheet.Title>
            <Sheet.Description>A press on the page will not close this sheet.</Sheet.Description>
        </Sheet.Header>
        <Input label="Display name" value="Maya Reyes" />
        <Sheet.Footer>
            <Sheet.Close>Cancel</Sheet.Close>
            <Sheet.Close variant="primary">Save</Sheet.Close>
        </Sheet.Footer>
    </Sheet.Content>
</Sheet.Root>
~~~~

For the rendered reference, visit [/docs/components/sheet](/docs/components/sheet).
