Skip to content
CodeAtlas UI

Feedback

Sheet

A mobile-first bottom sheet with swipe to dismiss that becomes a centered dialog from 768px.

<script>
  import { Sheet, Button } from "@codeatlasdev/ui";
  let open = $state(false);
</script>

<Sheet bind:open title="Switch session" description="Choose the active account">
  ...
  {#snippet footer()}<Button>New session</Button>{/snippet}
</Sheet>

Below 768px the sheet rises from the bottom edge with a drag handle, safe-area padding and rounded top corners. From 768px up it renders as a centered dialog. Focus trap, focus restore, Escape, overlay dismissal and scroll lock come from the native modal <dialog> the sheet renders.

Swipe to dismiss. Drag the handle or the header down. Releasing past 30% of the panel height, or faster than 0.5 px/ms, closes the sheet; otherwise it springs back. The gesture is off under prefers-reduced-motion and in the centered desktop layout. Keyboard users close it with Escape or the close button. Open the demos below on a phone or in a mobile viewport to try the gesture.

Swipe to dismiss

Drag the handle or header down on a viewport below 768px. Above that, it is a centered dialog.

Code
svelte
<script lang="ts">
  import { Sheet, Button, Stack } from '@codeatlasdev/ui';

  let open = $state(false);
  const accounts = ['Account A', 'Account B', 'Account C', 'Account D', 'Account E', 'Account F', 'Account G', 'Account H'];
</script>

<Button variant="outlined" onclick={() => (open = true)}>Open sheet</Button>

<Sheet bind:open title="Switch session" description="Choose the active account">
  <Stack gap="1">
    {#each accounts as acc}
      <button class="item" onclick={() => (open = false)}>{acc}</button>
    {/each}
  </Stack>
</Sheet>

<style>
  .item {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 9px 12px;
    background: transparent;
    border: none;
    border-radius: var(--ca-radius-md);
    font-size: var(--ca-font-size-body2);
    font-family: inherit;
    color: var(--ca-text-primary);
    cursor: pointer;
    text-align: left;
    transition: background var(--ca-duration-fast);
  }
  .item:hover { background: var(--ca-action-hover); }
</style>

A pinned footer, and swipeToDismiss set to false so only the close button, Escape and the overlay dismiss it.

Code
svelte
<script lang="ts">
  import { Sheet, Button, Stack } from '@codeatlasdev/ui';
  import { Plus } from '@lucide/svelte';

  let open = $state(false);
  let swipe = $state(true);
</script>

<Button variant="outlined" onclick={() => ((swipe = true), (open = true))}>Sheet with footer</Button>
<Button variant="outlined" onclick={() => ((swipe = false), (open = true))}>Swipe disabled</Button>

<Sheet bind:open title="Sessions" swipeToDismiss={swipe} onclose={() => (open = false)}>
  <Stack gap="2">
    <p class="text">
      {swipe
        ? 'Drag the handle or the header down to dismiss.'
        : 'swipeToDismiss is false: use the close button, Escape or the overlay.'}
    </p>
  </Stack>
  {#snippet footer()}
    <Button variant="filled" onclick={() => (open = false)}>
      {#snippet leading()}<Plus size={15} />{/snippet}
      New session
    </Button>
  {/snippet}
</Sheet>

<style>
  .text { margin: 0; font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>

Props

PropTypeDefaultDescription
openbooleanfalseBindable open state.
titlestring–Header title. Without it the panel falls back to a localized generic aria-label.
descriptionstring–Supporting text under the title.
swipeToDismissbooleantrueDrag the handle or header down to dismiss in the bottom-sheet layout. Ignored under reduced motion and on desktop.
onclose() => void–Called when the sheet closes by swipe, close button, Escape or overlay.
childrenSnippet–Required. Scrollable body.
footerSnippet–Pinned footer below the body.