Skip to content

Drawer

Experimental Alpha

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

Vue
Lit
React
Live Preview

Drawer from Start (Left)

Drawer from End (Right)

Open End Drawer

Preferences

CancelSave

Drawer from Top

Open Top Drawer
New message

You have a new message from Sarah

Update available

A new version is ready to install

Drawer from Bottom

Open Bottom Drawer
New Item
Upload
Download
Share

Drawer with Custom Content

Open Filter Drawer

Filters

Category

Price Range

ClearApply Filters
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>
  );
}
Open in StackBlitz

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:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add Drawer

The 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
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
tsx
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)
html
<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

PropTypeDefaultDescription
openbooleanfalseWhether 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
headingstring''The heading text for the drawer
descriptionstring''The description text for the drawer
noCloseOnEscapebooleanfalsePrevents closing the drawer when pressing the Escape key
noCloseOnBackdropbooleanfalsePrevents closing the drawer when clicking the backdrop
showCloseButtonbooleanfalseShows a close button (×) in the top-right corner of the drawer

Events

EventPayloadDescription
@drawer-open (Vue) / onDrawerOpen (React) / drawer-open (Lit)DrawerOpenEventEmitted when the drawer is opened
@drawer-close (Vue) / onDrawerClose (React) / drawer-close (Lit)DrawerCloseEventEmitted when the drawer is closed (via close button, Escape key, or backdrop click)
@drawer-cancel (Vue) / onDrawerCancel (React) / drawer-cancel (Lit)DrawerCancelEventEmitted 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 heading prop when used)
  • slot="footer": Footer content for action buttons

React

  • children: Main content of the drawer
  • DrawerHeader: Custom header content (replaces heading prop when used)
  • DrawerFooter: Footer content for action buttons

Lit

  • Default slot: Main content of the drawer
  • slot="header": Custom header content (replaces heading prop when used)
  • slot="footer": Footer content for action buttons

Accessibility

The Drawer component follows accessibility best practices for slide-out panels:

  • Uses role="dialog" and aria-modal="true" for proper screen reader announcement
  • Implements focus trap to keep keyboard focus within the drawer
  • Pressing Escape closes the drawer (unless noCloseOnEscape is true)
  • Clicking the backdrop closes the drawer (unless noCloseOnBackdrop is 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 heading prop 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 heading prop or custom header) for accessibility
  • Use showCloseButton or provide explicit close actions in footer for easy dismissal
  • Use position="start" or position="end" for primary navigation
  • Use position="top" or position="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