Skip to content
CodeAtlas UI

Feedback

Drawer

A side or bottom panel for the second layer, with a blurred overlay and a solid panel.

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

<Drawer bind:open title="Filters" side="right" width="360px">
  ...
  {#snippet actions()}<Button onclick={apply}>Apply</Button>{/snippet}
</Drawer>

Same architecture as Dialog: the overlay blurs the page and the panel is a solid surface that slides in from an edge. For a mobile bottom panel with swipe to dismiss, use Sheet.

Right (default)

Slides in from the right, with a footer actions area.

Code
svelte
<script lang="ts">
  import { Drawer, Button, Switch, Stack, FilterGroup } from '@codeatlasdev/ui';
  import { SlidersHorizontal } from '@lucide/svelte';

  let open = $state(false);
  let statusF = $state('all');
  let roleF = $state('all');
  const statusOpts = [{ value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' }];
  const roleOpts = [{ value: 'Admin', label: 'Admin' }, { value: 'Editor', label: 'Editor' }, { value: 'Viewer', label: 'Viewer' }];
</script>

<Button variant="outlined" onclick={() => (open = true)}>
  {#snippet leading()}<SlidersHorizontal size={14} />{/snippet}
  Open drawer
</Button>

<Drawer bind:open title="Filters">
  <Stack gap={16}>
    <div>
      <p class="label">Status</p>
      <FilterGroup options={statusOpts} value={statusF} onchange={(v) => (statusF = v)} />
    </div>
    <div>
      <p class="label">Role</p>
      <FilterGroup options={roleOpts} value={roleF} onchange={(v) => (roleF = v)} />
    </div>
    <Switch label="Show inactive only" />
    <Switch label="Include archived" />
  </Stack>
  {#snippet actions()}
    <Button variant="ghost" onclick={() => (open = false)}>Reset</Button>
    <Button variant="filled" onclick={() => (open = false)}>Apply</Button>
  {/snippet}
</Drawer>

<style>
  .label {
    font-size: 12px;
    font-weight: 500;
    color: var(--ca-text-secondary);
    margin: 0 0 8px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
  }
</style>

Left and bottom

Left suits mobile navigation. Bottom is a simple bottom panel without gestures.

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

  let leftOpen = $state(false);
  let bottomOpen = $state(false);
</script>

<Button variant="outlined" onclick={() => (leftOpen = true)}>Open left</Button>
<Button variant="outlined" onclick={() => (bottomOpen = true)}>Open bottom</Button>

<Drawer bind:open={leftOpen} title="Navigation" side="left" width="280px">
  <Stack gap={4}>
    {#each ['Dashboard', 'Users', 'Products', 'Reports', 'Settings'] as item}
      <button class="item" onclick={() => (leftOpen = false)}>{item}</button>
    {/each}
  </Stack>
</Drawer>

<Drawer bind:open={bottomOpen} title="Sort by" side="bottom">
  <Stack gap={4}>
    {#each ['Name (A-Z)', 'Name (Z-A)', 'Date joined', 'Last active', 'Role'] as opt}
      <button class="item" onclick={() => (bottomOpen = false)}>{opt}</button>
    {/each}
  </Stack>
</Drawer>

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

Notification panel

A real-world use: a 400px wide panel on the right.

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

  let open = $state(false);
  const items = [
    { title: 'New user joined', body: 'Ana Lima joined the workspace.', time: '2m ago', unread: true },
    { title: 'Export complete', body: 'Your CSV export is ready to download.', time: '18m ago', unread: true },
    { title: 'Billing updated', body: 'Your plan was upgraded to Pro.', time: '1h ago', unread: false },
    { title: 'Comment on #design', body: 'Carlos replied to your comment.', time: '3h ago', unread: false },
  ];
</script>

<Button variant="ghost" onclick={() => (open = true)}>
  {#snippet leading()}<Bell size={14} />{/snippet}
  Notifications
</Button>

<Drawer bind:open title="Notifications" side="right" width="400px">
  <Stack gap={0}>
    {#each items as n}
      <div class="notif" class:notif--unread={n.unread}>
        {#if n.unread}<span class="dot"></span>{/if}
        <div class="content">
          <p class="title">{n.title}</p>
          <p class="body">{n.body}</p>
        </div>
        <span class="time">{n.time}</span>
      </div>
    {/each}
  </Stack>
  {#snippet actions()}
    <Button variant="ghost" size="sm" onclick={() => (open = false)}>Mark all read</Button>
  {/snippet}
</Drawer>

<style>
  .notif {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 14px 0 14px 17px;
    border-bottom: 1px solid var(--ca-outline-subtle);
  }
  .notif:last-child { border-bottom: none; }
  .notif--unread { padding-left: 0; }
  .dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--ca-brand-main);
    flex-shrink: 0;
    margin-top: 5px;
  }
  .content { flex: 1; min-width: 0; }
  .title { font-size: 13px; font-weight: 500; color: var(--ca-text-primary); margin: 0 0 3px; }
  .body { font-size: 12px; color: var(--ca-text-secondary); margin: 0; line-height: 1.4; }
  .time { font-size: 11px; color: var(--ca-text-hint); flex-shrink: 0; margin-top: 2px; }
</style>

Props

PropTypeDefaultDescription
openbooleanfalseBindable open state.
side'left' | 'right' | 'bottom''right'Edge the panel slides from.
titlestring–Header title.
widthstring'360px'Panel width for left and right.
childrenSnippet–Panel body.
actionsSnippet–Footer buttons, separated by a top rule.
onOpenChange(open: boolean) => void–Called whenever the open state changes.
classstring''Extra classes on the panel.