Feedback
Drawer
A side or bottom panel for the second layer, with a blurred overlay and a solid panel.
<script>
import { Drawer, Button } 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.
#089Right (default)
Slides in from the right, with a footer actions area.
Reference #089 · Pick in the gallery
Code
<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="4">
<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>
#090Left and bottom
Left suits mobile navigation. Bottom is a simple bottom panel without gestures.
Reference #090 · Pick in the gallery
Code
<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="1">
{#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="1">
{#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>
#088Notification panel
A real-world use: a 400px wide panel on the right.
Reference #088 · Pick in the gallery
Code
<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>
#461No title: aria-labelledby points at your heading, so the panel keeps its accessible name while you build the header and its close button. Attributes such as data-testid reach the dialog.
Reference #461 · Pick in the gallery
Code
<script lang="ts">
import { Drawer, Button } from '@codeatlasdev/ui';
import { X } from '@lucide/svelte';
let open = $state(false);
</script>
<Button variant="outlined" onclick={() => (open = true)}>Open item</Button>
<!-- No `title`: aria-labelledby points at the drawer's own heading. -->
<Drawer bind:open aria-labelledby="item-heading" data-testid="item-drawer">
<header class="head">
<div>
<p class="key">PRJ-42</p>
<h2 id="item-heading" class="name">Fix login redirect</h2>
</div>
<Button variant="ghost" size="sm" aria-label="Close item" onclick={() => (open = false)}>
{#snippet leading()}<X size={14} />{/snippet}
</Button>
</header>
<p class="body">Own header, own close button. The panel is still named by the heading above.</p>
</Drawer>
<style>
.head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.key { margin: 0; font-size: 12px; color: var(--ca-text-secondary); letter-spacing: 0.05em; }
.name { margin: 2px 0 0; font-size: var(--ca-font-size-body1); color: var(--ca-text-primary); }
.body { margin: 16px 0 0; font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
The panel needs a name. Pass title (add hideTitle to keep it for screen readers only), or aria-label, or aria-labelledby with the id of your own heading. Any other attribute (id, data-*, style) reaches the <dialog> element.
Props
| Prop | Type | Default | Description |
|---|
open | boolean | false | Bindable open state. |
side | 'left' | 'right' | 'bottom' | 'right' | Edge the panel slides from. |
title | string | – | Header title and the panel's accessible name. |
hideTitle | boolean | false | Hides the title heading visually but keeps it as the accessible name; the header shrinks to the close button. |
aria-label | string | – | Accessible name when there is no `title`. Replaces the title reference. |
aria-labelledby | string | – | Id of your own heading, for a custom header inside the body. Wins over `title`. |
width | string | '360px' | Panel width for left and right. |
children | Snippet | – | Panel body. |
actions | Snippet | – | Footer buttons, separated by a top rule. |
onOpenChange | (open: boolean) => void | – | Called whenever the open state changes. |
class | string | '' | Extra classes on the panel. |