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 Bits UI Dialog.

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={4}>
    {#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={8}>
    <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.