Drawer
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
A drawer is a panel that slides in from the edge of the screen, typically used for navigation menus, filters, settings, or other supplementary content. Drawers can slide in from any of the four edges: start (left), end (right), top, or bottom.
Examples
Live Preview
Drawer from Start (Left)
Drawer from End (Right)
Preferences
Drawer from Top
You have a new message from Sarah
A new version is ready to install
Drawer from Bottom
Drawer with Custom Content
Filters
Category
Price Range
View Vue Code
<template>
<section>
<div class="mbe4">
<h2>Drawer from Start (Left)</h2>
</div>
<div class="stacked-mobile mbe4">
<VueButton @click="showStartDrawer">Open Start Drawer</VueButton>
<VueDrawer
:open="isStartDrawerOpen"
position="start"
heading="Navigation"
show-close-button
@drawer-close="isStartDrawerOpen = false"
>
<nav>
<ul style="list-style: none; padding: 0;">
<li style="padding: 0.5rem 0;"><a
href="#"
@click.prevent
>Dashboard</a></li>
<li style="padding: 0.5rem 0;"><a
href="#"
@click.prevent
>Projects</a></li>
<li style="padding: 0.5rem 0;"><a
href="#"
@click.prevent
>Team</a></li>
<li style="padding: 0.5rem 0;"><a
href="#"
@click.prevent
>Settings</a></li>
</ul>
</nav>
</VueDrawer>
</div>
<div class="mbe4">
<h2>Drawer from End (Right)</h2>
</div>
<div class="stacked-mobile mbe4">
<VueButton @click="showEndDrawer">Open End Drawer</VueButton>
<VueDrawer
:open="isEndDrawerOpen"
position="end"
heading="Settings"
show-close-button
@drawer-close="isEndDrawerOpen = false"
>
<div>
<h4 style="margin-top: 0;">Preferences</h4>
<label style="display: block; margin-bottom: 1rem;">
<input
type="checkbox"
style="margin-right: 0.5rem;"
/>
Enable notifications
</label>
<label style="display: block; margin-bottom: 1rem;">
<input
type="checkbox"
style="margin-right: 0.5rem;"
/>
Dark mode
</label>
<label style="display: block; margin-bottom: 1rem;">
<input
type="checkbox"
style="margin-right: 0.5rem;"
/>
Auto-save
</label>
</div>
<div slot="footer">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<VueButton @click="isEndDrawerOpen = false">Cancel</VueButton>
<VueButton
variant="primary"
@click="isEndDrawerOpen = false"
>Save</VueButton>
</div>
</div>
</VueDrawer>
</div>
<div class="mbe4">
<h2>Drawer from Top</h2>
</div>
<div class="stacked-mobile mbe4">
<VueButton @click="showTopDrawer">Open Top Drawer</VueButton>
<VueDrawer
:open="isTopDrawerOpen"
position="top"
heading="Notifications"
show-close-button
@drawer-close="isTopDrawerOpen = false"
>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="padding: 0.75rem; background: var(--ag-background-secondary); border-radius: 4px;">
<strong>New message</strong>
<p style="margin: 0.25rem 0 0 0; color: var(--ag-text-secondary); font-size: 0.875rem;">
You have a new message from Sarah
</p>
</div>
<div style="padding: 0.75rem; background: var(--ag-background-secondary); border-radius: 4px;">
<strong>Update available</strong>
<p style="margin: 0.25rem 0 0 0; color: var(--ag-text-secondary); font-size: 0.875rem;">
A new version is ready to install
</p>
</div>
</div>
</VueDrawer>
</div>
<div class="mbe4">
<h2>Drawer from Bottom</h2>
</div>
<div class="stacked-mobile mbe4">
<VueButton @click="showBottomDrawer">Open Bottom Drawer</VueButton>
<VueDrawer
:open="isBottomDrawerOpen"
position="bottom"
heading="Quick Actions"
show-close-button
@drawer-close="isBottomDrawerOpen = false"
>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<VueButton>
<div class="flex-inline items-center">
<Plus
:size="16"
class="mie2"
/>
New Item
</div>
</VueButton>
<VueButton>
<div class="flex-inline items-center">
<Upload
:size="16"
class="mie2"
/>
Upload
</div>
</VueButton>
<VueButton>
<div class="flex-inline items-center">
<Download
:size="16"
class="mie2"
/>
Download
</div>
</VueButton>
<VueButton>
<div class="flex-inline items-center">
<Share2
:size="16"
class="mie2"
/>
Share
</div>
</VueButton>
</div>
</VueDrawer>
</div>
<div class="mbe4">
<h2>Drawer with Custom Content</h2>
</div>
<div class="stacked-mobile mbe4">
<VueButton @click="showCustomDrawer">Open Filter Drawer</VueButton>
<VueDrawer
:open="isCustomDrawerOpen"
position="end"
show-close-button
@drawer-close="isCustomDrawerOpen = false"
>
<div slot="header">
<h2 style="margin: 0; font-size: var(--ag-font-size-lg);">
<div class="flex-inline items-center">
<Filter
:size="20"
class="mie2"
/>
Filters
</div>
</h2>
</div>
<div>
<div style="margin-bottom: 1.5rem;">
<h4 style="margin: 0 0 0.5rem 0;">Category</h4>
<label style="display: block; margin-bottom: 0.5rem;">
<input
type="checkbox"
style="margin-right: 0.5rem;"
/>
Electronics
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input
type="checkbox"
style="margin-right: 0.5rem;"
/>
Clothing
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input
type="checkbox"
style="margin-right: 0.5rem;"
/>
Books
</label>
</div>
<div style="margin-bottom: 1.5rem;">
<h4 style="margin: 0 0 0.5rem 0;">Price Range</h4>
<input
type="range"
min="0"
max="1000"
style="width: 100%;"
/>
</div>
</div>
<div slot="footer">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<VueButton @click="isCustomDrawerOpen = false">Clear</VueButton>
<VueButton
variant="primary"
@click="isCustomDrawerOpen = false"
>Apply Filters</VueButton>
</div>
</div>
</VueDrawer>
</div>
</section>
</template>
<script>
import VueDrawer from "agnosticui-core/drawer/vue";
import VueButton from "agnosticui-core/button/vue";
import { Plus, Upload, Download, Share2, Filter } from "lucide-vue-next";
export default {
name: "DrawerExamples",
components: {
VueDrawer,
VueButton,
Plus,
Upload,
Download,
Share2,
Filter,
},
data() {
return {
isStartDrawerOpen: false,
isEndDrawerOpen: false,
isTopDrawerOpen: false,
isBottomDrawerOpen: false,
isCustomDrawerOpen: false,
};
},
methods: {
showStartDrawer() {
this.isStartDrawerOpen = true;
},
showEndDrawer() {
this.isEndDrawerOpen = true;
},
showTopDrawer() {
this.isTopDrawerOpen = true;
},
showBottomDrawer() {
this.isBottomDrawerOpen = true;
},
showCustomDrawer() {
this.isCustomDrawerOpen = true;
},
},
};
</script>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/drawer';
import 'agnosticui-core/button';
export class DrawerLitExamples extends LitElement {
// Render in light DOM to access global utility classes
createRenderRoot() {
return this;
}
constructor() {
super();
this.isStartDrawerOpen = false;
this.isEndDrawerOpen = false;
this.isTopDrawerOpen = false;
this.isBottomDrawerOpen = false;
this.isCustomDrawerOpen = false;
}
firstUpdated() {
// Set up event listeners for drawer close events
this.querySelectorAll('ag-drawer').forEach((drawer) => {
drawer.addEventListener('drawer-close', (e) => {
const drawerId = e.target.id;
this.handleDrawerClose(drawerId);
});
});
}
handleDrawerClose(drawerId) {
switch (drawerId) {
case 'start-drawer':
this.isStartDrawerOpen = false;
break;
case 'end-drawer':
this.isEndDrawerOpen = false;
break;
case 'top-drawer':
this.isTopDrawerOpen = false;
break;
case 'bottom-drawer':
this.isBottomDrawerOpen = false;
break;
case 'custom-drawer':
this.isCustomDrawerOpen = false;
break;
}
this.requestUpdate();
}
showStartDrawer() {
this.isStartDrawerOpen = true;
this.requestUpdate();
setTimeout(() => {
const drawer = this.querySelector('#start-drawer');
if (drawer) drawer.open = true;
}, 0);
}
showEndDrawer() {
this.isEndDrawerOpen = true;
this.requestUpdate();
setTimeout(() => {
const drawer = this.querySelector('#end-drawer');
if (drawer) drawer.open = true;
}, 0);
}
showTopDrawer() {
this.isTopDrawerOpen = true;
this.requestUpdate();
setTimeout(() => {
const drawer = this.querySelector('#top-drawer');
if (drawer) drawer.open = true;
}, 0);
}
showBottomDrawer() {
this.isBottomDrawerOpen = true;
this.requestUpdate();
setTimeout(() => {
const drawer = this.querySelector('#bottom-drawer');
if (drawer) drawer.open = true;
}, 0);
}
showCustomDrawer() {
this.isCustomDrawerOpen = true;
this.requestUpdate();
setTimeout(() => {
const drawer = this.querySelector('#custom-drawer');
if (drawer) drawer.open = true;
}, 0);
}
render() {
return html`
<section>
<!-- Drawer from Start (Left) -->
<div class="mbe4">
<h2>Drawer from Start (Left)</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-button @click=${this.showStartDrawer}>Open Start Drawer</ag-button>
<ag-drawer
id="start-drawer"
position="start"
heading="Navigation"
show-close-button
>
<nav>
<ul style="list-style: none; padding: 0;">
<li style="padding: 0.5rem 0;"><a href="#" @click=${(e) => e.preventDefault()}>Dashboard</a></li>
<li style="padding: 0.5rem 0;"><a href="#" @click=${(e) => e.preventDefault()}>Projects</a></li>
<li style="padding: 0.5rem 0;"><a href="#" @click=${(e) => e.preventDefault()}>Team</a></li>
<li style="padding: 0.5rem 0;"><a href="#" @click=${(e) => e.preventDefault()}>Settings</a></li>
</ul>
</nav>
</ag-drawer>
</div>
<!-- Drawer from End (Right) -->
<div class="mbe4">
<h2>Drawer from End (Right)</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-button @click=${this.showEndDrawer}>Open End Drawer</ag-button>
<ag-drawer
id="end-drawer"
position="end"
heading="Settings"
show-close-button
>
<div>
<h4 style="margin-top: 0;">Preferences</h4>
<label style="display: block; margin-bottom: 1rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Enable notifications
</label>
<label style="display: block; margin-bottom: 1rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Dark mode
</label>
<label style="display: block; margin-bottom: 1rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Auto-save
</label>
</div>
<div slot="footer">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<ag-button @click=${() => {
const drawer = this.querySelector('#end-drawer');
if (drawer) drawer.open = false;
}}>Cancel</ag-button>
<ag-button
variant="primary"
@click=${() => {
const drawer = this.querySelector('#end-drawer');
if (drawer) drawer.open = false;
}}
>Save</ag-button>
</div>
</div>
</ag-drawer>
</div>
<!-- Drawer from Top -->
<div class="mbe4">
<h2>Drawer from Top</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-button @click=${this.showTopDrawer}>Open Top Drawer</ag-button>
<ag-drawer
id="top-drawer"
position="top"
heading="Notifications"
show-close-button
>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="padding: 0.75rem; background: var(--ag-background-secondary); border-radius: 4px;">
<strong>New message</strong>
<p style="margin: 0.25rem 0 0 0; color: var(--ag-text-secondary); font-size: 0.875rem;">
You have a new message from Sarah
</p>
</div>
<div style="padding: 0.75rem; background: var(--ag-background-secondary); border-radius: 4px;">
<strong>Update available</strong>
<p style="margin: 0.25rem 0 0 0; color: var(--ag-text-secondary); font-size: 0.875rem;">
A new version is ready to install
</p>
</div>
</div>
</ag-drawer>
</div>
<!-- Drawer from Bottom -->
<div class="mbe4">
<h2>Drawer from Bottom</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-button @click=${this.showBottomDrawer}>Open Bottom Drawer</ag-button>
<ag-drawer
id="bottom-drawer"
position="bottom"
heading="Quick Actions"
show-close-button
>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<ag-button>
<div class="flex-inline items-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mie2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New Item
</div>
</ag-button>
<ag-button>
<div class="flex-inline items-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mie2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Upload
</div>
</ag-button>
<ag-button>
<div class="flex-inline items-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mie2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download
</div>
</ag-button>
<ag-button>
<div class="flex-inline items-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mie2">
<circle cx="18" cy="5" r="3"></circle>
<circle cx="6" cy="12" r="3"></circle>
<circle cx="18" cy="19" r="3"></circle>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
</svg>
Share
</div>
</ag-button>
</div>
</ag-drawer>
</div>
<!-- Drawer with Custom Content -->
<div class="mbe4">
<h2>Drawer with Custom Content</h2>
</div>
<div class="stacked-mobile mbe4">
<ag-button @click=${this.showCustomDrawer}>Open Filter Drawer</ag-button>
<ag-drawer
id="custom-drawer"
position="end"
show-close-button
>
<div slot="header">
<h2 style="margin: 0; font-size: var(--ag-font-size-lg);">
<div class="flex-inline items-center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mie2">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
Filters
</div>
</h2>
</div>
<div>
<div style="margin-bottom: 1.5rem;">
<h4 style="margin: 0 0 0.5rem 0;">Category</h4>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Electronics
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Clothing
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Books
</label>
</div>
<div style="margin-bottom: 1.5rem;">
<h4 style="margin: 0 0 0.5rem 0;">Price Range</h4>
<input type="range" min="0" max="1000" style="width: 100%;" />
</div>
</div>
<div slot="footer">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<ag-button @click=${() => {
const drawer = this.querySelector('#custom-drawer');
if (drawer) drawer.open = false;
}}>Clear</ag-button>
<ag-button
variant="primary"
@click=${() => {
const drawer = this.querySelector('#custom-drawer');
if (drawer) drawer.open = false;
}}
>Apply Filters</ag-button>
</div>
</div>
</ag-drawer>
</div>
</section>
`;
}
}
// Register the custom element
customElements.define('drawer-lit-examples', DrawerLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { useState } from "react";
import { ReactDrawer } from "agnosticui-core/drawer/react";
import { ReactButton } from "agnosticui-core/button/react";
export default function DrawerReactExamples() {
const [isStartDrawerOpen, setIsStartDrawerOpen] = useState(false);
const [isEndDrawerOpen, setIsEndDrawerOpen] = useState(false);
const [isTopDrawerOpen, setIsTopDrawerOpen] = useState(false);
const [isBottomDrawerOpen, setIsBottomDrawerOpen] = useState(false);
const [isCustomDrawerOpen, setIsCustomDrawerOpen] = useState(false);
return (
<section>
{/* Drawer from Start (Left) */}
<div className="mbe4">
<h2>Drawer from Start (Left)</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactButton onClick={() => setIsStartDrawerOpen(true)}>
Open Start Drawer
</ReactButton>
<ReactDrawer
open={isStartDrawerOpen}
onOpenChange={setIsStartDrawerOpen}
position="start"
heading="Navigation"
showCloseButton
>
<nav>
<ul style={{ listStyle: "none", padding: 0 }}>
<li style={{ padding: "0.5rem 0" }}>
<a href="#" onClick={(e) => e.preventDefault()}>
Dashboard
</a>
</li>
<li style={{ padding: "0.5rem 0" }}>
<a href="#" onClick={(e) => e.preventDefault()}>
Projects
</a>
</li>
<li style={{ padding: "0.5rem 0" }}>
<a href="#" onClick={(e) => e.preventDefault()}>
Team
</a>
</li>
<li style={{ padding: "0.5rem 0" }}>
<a href="#" onClick={(e) => e.preventDefault()}>
Settings
</a>
</li>
</ul>
</nav>
</ReactDrawer>
</div>
{/* Drawer from End (Right) */}
<div className="mbe4">
<h2>Drawer from End (Right)</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactButton onClick={() => setIsEndDrawerOpen(true)}>
Open End Drawer
</ReactButton>
<ReactDrawer
open={isEndDrawerOpen}
onOpenChange={setIsEndDrawerOpen}
position="end"
heading="Settings"
showCloseButton
footer={
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end" }}>
<ReactButton onClick={() => setIsEndDrawerOpen(false)}>
Cancel
</ReactButton>
<ReactButton variant="primary" onClick={() => setIsEndDrawerOpen(false)}>
Save
</ReactButton>
</div>
}
>
<div>
<h4 style={{ marginTop: 0 }}>Preferences</h4>
<label style={{ display: "block", marginBottom: "1rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Enable notifications
</label>
<label style={{ display: "block", marginBottom: "1rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Dark mode
</label>
<label style={{ display: "block", marginBottom: "1rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Auto-save
</label>
</div>
</ReactDrawer>
</div>
{/* Drawer from Top */}
<div className="mbe4">
<h2>Drawer from Top</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactButton onClick={() => setIsTopDrawerOpen(true)}>
Open Top Drawer
</ReactButton>
<ReactDrawer
open={isTopDrawerOpen}
onOpenChange={setIsTopDrawerOpen}
position="top"
heading="Notifications"
showCloseButton
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div
style={{
padding: "0.75rem",
background: "var(--ag-background-secondary)",
borderRadius: "4px",
}}
>
<strong>New message</strong>
<p
style={{
margin: "0.25rem 0 0 0",
color: "var(--ag-text-secondary)",
fontSize: "0.875rem",
}}
>
You have a new message from Sarah
</p>
</div>
<div
style={{
padding: "0.75rem",
background: "var(--ag-background-secondary)",
borderRadius: "4px",
}}
>
<strong>Update available</strong>
<p
style={{
margin: "0.25rem 0 0 0",
color: "var(--ag-text-secondary)",
fontSize: "0.875rem",
}}
>
A new version is ready to install
</p>
</div>
</div>
</ReactDrawer>
</div>
{/* Drawer from Bottom */}
<div className="mbe4">
<h2>Drawer from Bottom</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactButton onClick={() => setIsBottomDrawerOpen(true)}>
Open Bottom Drawer
</ReactButton>
<ReactDrawer
open={isBottomDrawerOpen}
onOpenChange={setIsBottomDrawerOpen}
position="bottom"
heading="Quick Actions"
showCloseButton
>
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
<ReactButton>
<div className="flex-inline items-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="mie2"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New Item
</div>
</ReactButton>
<ReactButton>
<div className="flex-inline items-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="mie2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Upload
</div>
</ReactButton>
<ReactButton>
<div className="flex-inline items-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="mie2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download
</div>
</ReactButton>
<ReactButton>
<div className="flex-inline items-center">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="mie2"
>
<circle cx="18" cy="5" r="3"></circle>
<circle cx="6" cy="12" r="3"></circle>
<circle cx="18" cy="19" r="3"></circle>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
</svg>
Share
</div>
</ReactButton>
</div>
</ReactDrawer>
</div>
{/* Drawer with Custom Content */}
<div className="mbe4">
<h2>Drawer with Custom Content</h2>
</div>
<div className="stacked-mobile mbe4">
<ReactButton onClick={() => setIsCustomDrawerOpen(true)}>
Open Filter Drawer
</ReactButton>
<ReactDrawer
open={isCustomDrawerOpen}
onOpenChange={setIsCustomDrawerOpen}
position="end"
showCloseButton
header={
<h2 style={{ margin: 0, fontSize: "var(--ag-font-size-lg)" }}>
<div className="flex-inline items-center">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="mie2"
>
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
Filters
</div>
</h2>
}
footer={
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end" }}>
<ReactButton onClick={() => setIsCustomDrawerOpen(false)}>
Clear
</ReactButton>
<ReactButton variant="primary" onClick={() => setIsCustomDrawerOpen(false)}>
Apply Filters
</ReactButton>
</div>
}
>
<div>
<div style={{ marginBottom: "1.5rem" }}>
<h4 style={{ margin: "0 0 0.5rem 0" }}>Category</h4>
<label style={{ display: "block", marginBottom: "0.5rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Electronics
</label>
<label style={{ display: "block", marginBottom: "0.5rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Clothing
</label>
<label style={{ display: "block", marginBottom: "0.5rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Books
</label>
</div>
<div style={{ marginBottom: "1.5rem" }}>
<h4 style={{ margin: "0 0 0.5rem 0" }}>Price Range</h4>
<input type="range" min="0" max="1000" style={{ width: "100%" }} />
</div>
</div>
</ReactDrawer>
</div>
</section>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add DrawerThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<section>
<VueButton @click="showStartDrawer">Open Navigation</VueButton>
<VueDrawer
:open="isStartDrawerOpen"
position="start"
heading="Navigation"
show-close-button
@drawer-close="isStartDrawerOpen = false"
>
<nav>
<ul style="list-style: none; padding: 0;">
<li style="padding: 0.5rem 0;"><a href="#">Dashboard</a></li>
<li style="padding: 0.5rem 0;"><a href="#">Projects</a></li>
<li style="padding: 0.5rem 0;"><a href="#">Settings</a></li>
</ul>
</nav>
</VueDrawer>
<VueButton @click="showEndDrawer">Open Settings</VueButton>
<VueDrawer
:open="isEndDrawerOpen"
position="end"
heading="Settings"
show-close-button
@drawer-close="isEndDrawerOpen = false"
>
<div>
<h4>Preferences</h4>
<label style="display: block; margin-bottom: 1rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Enable notifications
</label>
</div>
<div slot="footer">
<div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
<VueButton @click="isEndDrawerOpen = false">Cancel</VueButton>
<VueButton variant="primary" @click="isEndDrawerOpen = false"
>Save</VueButton
>
</div>
</div>
</VueDrawer>
<VueButton @click="showTopDrawer">Open Notifications</VueButton>
<VueDrawer
:open="isTopDrawerOpen"
position="top"
heading="Notifications"
show-close-button
@drawer-close="isTopDrawerOpen = false"
>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="padding: 0.75rem; background: #f3f4f6;">
<strong>New message</strong>
<p style="margin: 0.25rem 0 0 0;">You have a new message</p>
</div>
</div>
</VueDrawer>
<VueButton @click="showBottomDrawer">Open Quick Actions</VueButton>
<VueDrawer
:open="isBottomDrawerOpen"
position="bottom"
heading="Quick Actions"
show-close-button
@drawer-close="isBottomDrawerOpen = false"
>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<VueButton>New Item</VueButton>
<VueButton>Upload</VueButton>
<VueButton>Download</VueButton>
</div>
</VueDrawer>
</section>
</template>
<script>
import VueDrawer from "agnosticui-core/drawer/vue";
import VueButton from "agnosticui-core/button/vue";
export default {
components: {
VueDrawer,
VueButton,
},
data() {
return {
isStartDrawerOpen: false,
isEndDrawerOpen: false,
isTopDrawerOpen: false,
isBottomDrawerOpen: false,
};
},
methods: {
showStartDrawer() {
this.isStartDrawerOpen = true;
},
showEndDrawer() {
this.isEndDrawerOpen = true;
},
showTopDrawer() {
this.isTopDrawerOpen = true;
},
showBottomDrawer() {
this.isBottomDrawerOpen = true;
},
},
};
</script>React
import { useState } from "react";
import {
ReactDrawer,
DrawerHeader,
DrawerFooter,
} from "agnosticui-core/drawer/react";
export default function DrawerExample() {
const [isStartDrawerOpen, setIsStartDrawerOpen] = useState(false);
const [isEndDrawerOpen, setIsEndDrawerOpen] = useState(false);
const [isTopDrawerOpen, setIsTopDrawerOpen] = useState(false);
const [isBottomDrawerOpen, setIsBottomDrawerOpen] = useState(false);
return (
<section>
<button onClick={() => setIsStartDrawerOpen(true)}>
Open Navigation
</button>
<ReactDrawer
open={isStartDrawerOpen}
position="start"
heading="Navigation"
showCloseButton={true}
onDrawerClose={() => setIsStartDrawerOpen(false)}
>
<nav>
<ul style={{ listStyle: "none", padding: 0 }}>
<li style={{ padding: "0.5rem 0" }}>
<a href="#">Dashboard</a>
</li>
<li style={{ padding: "0.5rem 0" }}>
<a href="#">Projects</a>
</li>
<li style={{ padding: "0.5rem 0" }}>
<a href="#">Settings</a>
</li>
</ul>
</nav>
</ReactDrawer>
<button onClick={() => setIsEndDrawerOpen(true)}>Open Settings</button>
<ReactDrawer
open={isEndDrawerOpen}
position="end"
heading="Settings"
showCloseButton={true}
onDrawerClose={() => setIsEndDrawerOpen(false)}
>
<div>
<h4>Preferences</h4>
<label style={{ display: "block", marginBottom: "1rem" }}>
<input type="checkbox" style={{ marginRight: "0.5rem" }} />
Enable notifications
</label>
</div>
<DrawerFooter>
<div
style={{
display: "flex",
gap: "0.5rem",
justifyContent: "flex-end",
}}
>
<button onClick={() => setIsEndDrawerOpen(false)}>Cancel</button>
<button onClick={() => setIsEndDrawerOpen(false)}>Save</button>
</div>
</DrawerFooter>
</ReactDrawer>
<button onClick={() => setIsTopDrawerOpen(true)}>
Open Notifications
</button>
<ReactDrawer
open={isTopDrawerOpen}
position="top"
heading="Notifications"
showCloseButton={true}
onDrawerClose={() => setIsTopDrawerOpen(false)}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div style={{ padding: "0.75rem", background: "#f3f4f6" }}>
<strong>New message</strong>
<p style={{ margin: "0.25rem 0 0 0" }}>You have a new message</p>
</div>
</div>
</ReactDrawer>
<button onClick={() => setIsBottomDrawerOpen(true)}>
Open Quick Actions
</button>
<ReactDrawer
open={isBottomDrawerOpen}
position="bottom"
heading="Quick Actions"
showCloseButton={true}
onDrawerClose={() => setIsBottomDrawerOpen(false)}
>
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
<button>New Item</button>
<button>Upload</button>
<button>Download</button>
</div>
</ReactDrawer>
</section>
);
}Lit (Web Components)
<script type="module">
import 'agnosticui-core/drawer';
@customElement('my-element')
export class MyElement extends LitElement {
firstUpdated() {
// Query all drawers and buttons within the shadow DOM
const startDrawer = this.shadowRoot?.querySelector('#start-drawer') as any;
const endDrawer = this.shadowRoot?.querySelector('#end-drawer') as any;
const topDrawer = this.shadowRoot?.querySelector('#top-drawer') as any;
const bottomDrawer = this.shadowRoot?.querySelector('#bottom-drawer') as any;
const openStartBtn = this.shadowRoot?.querySelector('#open-start');
const openEndBtn = this.shadowRoot?.querySelector('#open-end');
const openTopBtn = this.shadowRoot?.querySelector('#open-top');
const openBottomBtn = this.shadowRoot?.querySelector('#open-bottom');
// Add click handlers for each button
openStartBtn?.addEventListener('click', () => {
if (startDrawer) startDrawer.open = true;
});
openEndBtn?.addEventListener('click', () => {
if (endDrawer) endDrawer.open = true;
});
openTopBtn?.addEventListener('click', () => {
if (topDrawer) topDrawer.open = true;
});
openBottomBtn?.addEventListener('click', () => {
if (bottomDrawer) bottomDrawer.open = true;
});
// Add close handlers for each drawer
[startDrawer, endDrawer, topDrawer, bottomDrawer].forEach(drawer => {
drawer?.addEventListener('drawer-close', () => {
drawer.open = false;
console.log('Drawer closed');
});
});
}
// ... rest of your class ...
}
</script>
<section>
<button id="open-start">Open Navigation</button>
<ag-drawer
id="start-drawer"
position="start"
heading="Navigation"
show-close-button
>
<nav>
<ul style="list-style: none; padding: 0;">
<li style="padding: 0.5rem 0;"><a href="#">Dashboard</a></li>
<li style="padding: 0.5rem 0;"><a href="#">Projects</a></li>
<li style="padding: 0.5rem 0;"><a href="#">Settings</a></li>
</ul>
</nav>
</ag-drawer>
<button id="open-end">Open Settings</button>
<ag-drawer
id="end-drawer"
position="end"
heading="Settings"
show-close-button
>
<div>
<h4>Preferences</h4>
<label style="display: block; margin-bottom: 1rem;">
<input type="checkbox" style="margin-right: 0.5rem;" />
Enable notifications
</label>
</div>
<div
slot="footer"
style="display: flex; gap: 0.5rem; justify-content: flex-end;"
>
<button>Cancel</button>
<button>Save</button>
</div>
</ag-drawer>
<button id="open-top">Open Notifications</button>
<ag-drawer
id="top-drawer"
position="top"
heading="Notifications"
show-close-button
>
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="padding: 0.75rem; background: #f3f4f6;">
<strong>New message</strong>
<p style="margin: 0.25rem 0 0 0;">You have a new message</p>
</div>
</div>
</ag-drawer>
<button id="open-bottom">Open Quick Actions</button>
<ag-drawer
id="bottom-drawer"
position="bottom"
heading="Quick Actions"
show-close-button
>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<button>New Item</button>
<button>Upload</button>
<button>Download</button>
</div>
</ag-drawer>
</section>Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Whether the drawer is open |
position | 'start' | 'end' | 'top' | 'bottom' | 'bottom' | The edge from which the drawer slides in. 'start' is left in LTR, 'end' is right in LTR |
heading | string | '' | The heading text for the drawer |
description | string | '' | The description text for the drawer |
noCloseOnEscape | boolean | false | Prevents closing the drawer when pressing the Escape key |
noCloseOnBackdrop | boolean | false | Prevents closing the drawer when clicking the backdrop |
showCloseButton | boolean | false | Shows a close button (×) in the top-right corner of the drawer |
Events
| Event | Payload | Description |
|---|---|---|
@drawer-open (Vue) / onDrawerOpen (React) / drawer-open (Lit) | DrawerOpenEvent | Emitted when the drawer is opened |
@drawer-close (Vue) / onDrawerClose (React) / drawer-close (Lit) | DrawerCloseEvent | Emitted when the drawer is closed (via close button, Escape key, or backdrop click) |
@drawer-cancel (Vue) / onDrawerCancel (React) / drawer-cancel (Lit) | DrawerCancelEvent | Emitted when the drawer is cancelled (via Escape key or backdrop click, but not via close button) |
Slots
Vue
- Default slot: Main content of the drawer
- slot="header": Custom header content (replaces
headingprop when used) - slot="footer": Footer content for action buttons
React
- children: Main content of the drawer
- DrawerHeader: Custom header content (replaces
headingprop when used) - DrawerFooter: Footer content for action buttons
Lit
- Default slot: Main content of the drawer
- slot="header": Custom header content (replaces
headingprop when used) - slot="footer": Footer content for action buttons
Accessibility
The Drawer component follows accessibility best practices for slide-out panels:
- Uses
role="dialog"andaria-modal="true"for proper screen reader announcement - Implements focus trap to keep keyboard focus within the drawer
- Pressing Escape closes the drawer (unless
noCloseOnEscapeis true) - Clicking the backdrop closes the drawer (unless
noCloseOnBackdropis true) - Returns focus to the triggering element when closed
- Prevents background scroll when drawer is open
- Close button has
aria-label="Close dialog"for screen readers - Drawer can be labeled via
headingprop or custom header with proper heading element - Keyboard navigation cycles through all focusable elements within the drawer
- Supports Tab and Shift+Tab for navigation within the focus trap
Best Practices
- Always provide a heading (via
headingprop or custom header) for accessibility - Use
showCloseButtonor provide explicit close actions in footer for easy dismissal - Use
position="start"orposition="end"for primary navigation - Use
position="top"orposition="bottom"for contextual actions or notifications - Consider the mobile experience - drawers from start/end work well, top/bottom may be harder to reach
- Keep drawer content focused and avoid nesting multiple levels of navigation
- For filters or settings, provide clear "Apply" and "Cancel" actions in the footer
Use Cases
- Navigation (
start/end): Primary navigation menu, contextual actions - Settings (
end): User preferences, application settings - Filters (
end): Search filters, data table filters - Notifications (
top): System notifications, alerts - Quick Actions (
bottom): Mobile-friendly action menus, sharing options