Start typing to search components and docs…
select navigate ESC close

Menu Drawer

Opinionated off-canvas menu built on top of Drawer. Drop one in with a single <MenuDrawer> tag and pass primary / chips / helpCard / footer props — the component renders the whole composition (brand header, primary nav with titles + subtitles, service chips, sticky help-card CTA, footer with language switcher + theme toggle + socials).

Inherits all Drawer behavior: trigger via aria-controls, click-backdrop / Escape / [data-drawer-close] to close, focus management, body.drawer-open scroll lock.

MenuDrawer.astro is Astro-only (uses <script> via Drawer). Import via subpath, not the barrel.

Full menu

Mirrors the uper.pl-style blog menu — brand on top, three primary links with subtitles, popular-services chips, a help card pushed to the bottom by a flex spacer, and a footer with locales + theme toggle + socials.

---
import MenuDrawer from 'spoko-design-system/components/MenuDrawer.astro';
---
<button type="button" class="btn-primary btn-normal" aria-controls="demo-full-menu" aria-expanded="false">
Open menu
</button>
<MenuDrawer
id="demo-full-menu"
label="Main menu"
brand={{ text: 'Studio', href: '/' }}
primary={[
  { title: 'SEO Audit', sub: 'Find what is blocking your site', href: '/seo-audit/' },
  { title: 'About', sub: 'Solo SEO freelancer',                href: '/about/' },
  { title: 'Blog',    sub: 'Articles on SEO and ranking',      href: '/blog/' },
]}
chips={{
  title: 'Popular services',
  items: [
    { label: 'SXO Audit',         href: '/sxo-audit/' },
    { label: 'Local SEO',         href: '/local-seo/' },
    { label: 'Changelog',         href: '/changelog/' },
  ],
}}
helpCard={{
  title: 'Want more Google traffic?',
  sub: 'Start with a free consultation.',
  cta: { label: 'Get in touch', href: '/contact/' },
}}
footer={{
  locales: [
    { code: 'en', label: 'EN', href: '/blog/',    active: true },
    { code: 'pl', label: 'PL', href: '/pl/blog/' },
  ],
  themeToggle: true,
  socials: [
    { label: 'Facebook', href: 'https://facebook.com',  iconPath: 'M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z' },
    { label: 'LinkedIn', href: 'https://linkedin.com',  iconPath: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z' },
  ],
}}
/>

Service-page layout — full composition

Right-aligned drawer combining every section in one tag: brand wordmark, three primary nav items with subtitles, popular-topic chips, a sticky help-card CTA, and a footer with locale switcher + theme toggle + socials. Brand uses text (rendered as a brand-blue wordmark via .sds-menu-brand-text); production replaces it with a real logo via logoSrc.

<button type="button" class="btn-primary btn-normal" aria-controls="demo-service-menu" aria-expanded="false">
Open menu
</button>
<MenuDrawer
id="demo-service-menu"
label="Navigation"
position="right"
brand={{ text: 'Brand', href: '/' }}
primary={[
  { title: 'Services',  sub: 'What we offer',           href: '/services/' },
  { title: 'About',     sub: 'Team, story and values',  href: '/about/' },
  { title: 'Insights',  sub: 'Articles and case studies', href: '/insights/' },
]}
chips={{
  title: 'Popular topics',
  items: [
    { label: 'Strategy',     href: '/strategy/' },
    { label: 'Performance',  href: '/performance/' },
    { label: 'Changelog',    href: '/changelog/' },
  ],
}}
helpCard={{
  title: 'Need help getting started?',
  sub: 'Book a free 30-minute call.',
  cta: { label: 'Get in touch', href: '/contact/' },
}}
footer={{
  locales: [
    { code: 'en', label: 'EN', href: '/',    active: true },
    { code: 'de', label: 'DE', href: '/de/' },
  ],
  themeToggle: true,
  socials: [
    { label: 'Facebook', href: '#facebook', iconPath: 'M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z' },
    { label: 'LinkedIn', href: '#linkedin', iconPath: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z' },
  ],
}}
/>

Minimal — only primary nav

Skip every optional section. The flex spacer keeps the footer pinned to the bottom even when the body is short, so a single-section menu still looks intentional.

<button type="button" class="btn-primary btn-normal" aria-controls="demo-minimal-menu" aria-expanded="false">
Open minimal menu
</button>
<MenuDrawer
id="demo-minimal-menu"
brand={{ text: 'Studio' }}
primary={[
  { title: 'Home',     href: '/' },
  { title: 'Projects', href: '/projects/' },
  { title: 'Contact',  href: '/contact/' },
]}
/>

Right-aligned

Pass position="right" to slide in from the opposite edge. Everything else unchanged.

<button type="button" class="btn-primary btn-normal" aria-controls="demo-right-menu" aria-expanded="false">
Open right menu
</button>
<MenuDrawer
id="demo-right-menu"
position="right"
brand={{ text: 'Catalog' }}
primary={[
  { title: 'Parts',     sub: '4,200+ entries',         href: '/parts/' },
  { title: 'Gearboxes', sub: 'Family tree + versions', href: '/gearboxes/' },
  { title: 'Sale',      sub: 'Used parts marketplace', href: '/sale/' },
]}
footer={{
  locales: [
    { code: 'en', label: 'EN', href: '/',    active: true },
    { code: 'pl', label: 'PL', href: '/pl/' },
    { code: 'de', label: 'DE', href: '/de/' },
  ],
}}
/>

Slot overrides

Any section can be replaced by passing a named slot. Useful when the prop shape doesn’t fit — e.g. brand needs an SVG logo, primary nav needs client-only icons, or the help card hosts a form.

<MenuDrawer id="custom" label="Custom menu">
  <a slot="brand" href="/">
    <MyCustomLogo class="h-8" />
  </a>

  <slot name="primary">
    <a href="/" class="sds-menu-primary-link" data-drawer-close>
      <div class="sds-menu-primary-text">
        <span class="sds-menu-primary-title">Home</span>
      </div>
    </a>
  </slot>

  <div slot="help" class="sds-menu-help-card">
    <NewsletterForm />
  </div>

  <div slot="footer-right">
    <CustomSocialIcons />
  </div>
</MenuDrawer>

Wiring the trigger

The drawer is opened by ANY element with aria-controls pointing at its id (handled by the underlying Drawer script). Couples naturally with Navbar’s drawerId prop:

<Navbar brand="Studio" drawerId="main-menu">
  <div slot="actions">
    <button class="btn-primary btn-sm">Sign in</button>
  </div>
</Navbar>

<MenuDrawer
  id="main-menu"
  brand={{ text: 'Studio' }}
  primary={[...]}
/>

Props

Prop Type Default Description
id * string Unique id used by triggers (aria-controls) and the open/close script
label string 'Menu' aria-label for the dialog — describes the menu to screen readers
position 'left' | 'right' 'left' Edge the drawer slides in from
brand { href?, logoSrc?, logoAlt?, text? } undefined Header brand mark. Pass `logoSrc` for an image or `text` for a wordmark. Omit to hide the header (close button still rendered).
primary PrimaryItem[] [] Primary nav items — each `{ title, sub?, href }`. Renders title + subtitle + chevron arrow.
chips { title, items: { label, href }[] } undefined Optional service chips section under the primary nav.
helpCard { title, sub?, cta: { label, href } } undefined Optional help-card CTA pinned near the bottom by the flex spacer.
footer { locales?, themeToggle?, socials? } undefined Optional footer cluster. `locales[]` renders a segmented PL/EN-style switcher; `themeToggle` adds a sun/moon button wired to `html.dark`; `socials[]` accepts `{ label, href, iconPath? | iconSvg? }`.

Slots

SlotReplaces
brandHeader brand area (close button stays)
primaryPrimary nav links
chipsService chips section (title + chips)
middleExtra content between chips and the spacer
helpHelp card CTA
footer-leftFooter left side (locales + theme toggle)
footer-rightFooter right side (socials)

CSS recipe classes

ClassPurpose
sds-drawer-menuMarker on .sds-drawer — applies the 380px menu panel width + padding
sds-menuVertical layout container inside the panel
sds-menu-header / sds-menu-brand / sds-menu-closeHeader row
sds-menu-primary / sds-menu-primary-link / sds-menu-primary-{title,sub,arrow}Primary nav anatomy
sds-menu-section / sds-menu-section-title / sds-menu-chips / sds-menu-chipChips section
sds-menu-spacerFlex spacer pushing footer down
sds-menu-help-card / sds-menu-help-{text,title,sub,cta}Help card CTA
sds-menu-footer / sds-menu-footer-utility / sds-menu-footer-socialsFooter row
sds-menu-lang / sds-menu-lang-btn + .is-activeSegmented language switcher
sds-menu-theme-chipDark mode toggle (wired by data-sds-menu-theme-toggle)