Menu

Dropdown menu with CSS Anchor Positioning and automatic viewport flipping

Overview

The s-menu component provides dropdown menus using native <details> disclosure with CSS Anchor Positioning for automatic viewport edge detection and flipping. Works in all browsers with graceful fallback.

Import

/* Full framework (includes all components) */
@import "@shift-css/core";

/* Or import just the menu component */
@import "@shift-css/core/components/menu";

Note: When importing individual components, you also need @shift-css/core/reset and @shift-css/core/tokens for the base styles and design tokens.

Basic Menu

Click the trigger to open. Uses native <details> with CSS positioning.

Basic dropdown menu

Options ▾
<details s-menu>
<summary s-menu-trigger s-btn="secondary">Options ▾</summary>
<div s-menu-items>
  <button s-menu-item onclick="this.closest('details').open=false">Edit</button>
  <button s-menu-item onclick="this.closest('details').open=false">Duplicate</button>
  <button s-menu-item onclick="this.closest('details').open=false">Archive</button>
</div>
</details>

Position Variants

Menu positioning options

Default ▾
End ▾
Top ▴
Top End ▴
<div class="demo-row" style="padding-top: 4.5rem;">
<details s-menu>
  <summary s-menu-trigger s-btn="secondary">Default ▾</summary>
  <div s-menu-items>
    <button s-menu-item onclick="this.closest('details').open=false">Bottom-start</button>
    <button s-menu-item onclick="this.closest('details').open=false">Aligned left</button>
  </div>
</details>
<details s-menu="end">
  <summary s-menu-trigger s-btn="secondary">End ▾</summary>
  <div s-menu-items>
    <button s-menu-item onclick="this.closest('details').open=false">Bottom-end</button>
    <button s-menu-item onclick="this.closest('details').open=false">Aligned right</button>
  </div>
</details>
<details s-menu="top">
  <summary s-menu-trigger s-btn="secondary">Top ▴</summary>
  <div s-menu-items>
    <button s-menu-item onclick="this.closest('details').open=false">Opens above</button>
    <button s-menu-item onclick="this.closest('details').open=false">Aligned left</button>
  </div>
</details>
<details s-menu="top-end">
  <summary s-menu-trigger s-btn="secondary">Top End ▴</summary>
  <div s-menu-items>
    <button s-menu-item onclick="this.closest('details').open=false">Opens above</button>
    <button s-menu-item onclick="this.closest('details').open=false">Aligned right</button>
  </div>
</details>
</div>

With Dividers & Labels

Organized menu with sections

Actions ▾
Document
Danger Zone
<details s-menu>
<summary s-menu-trigger s-btn="primary">Actions ▾</summary>
<div s-menu-items>
  <span s-menu-label>Document</span>
  <button s-menu-item onclick="this.closest('details').open=false">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
      <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
    </svg>
    Edit
  </button>
  <button s-menu-item onclick="this.closest('details').open=false">Duplicate</button>
  <button s-menu-item onclick="this.closest('details').open=false">Move to...</button>
  <hr s-menu-divider>
  <span s-menu-label>Danger Zone</span>
  <button s-menu-item onclick="this.closest('details').open=false">Archive</button>
  <button s-menu-item s-danger onclick="this.closest('details').open=false">Delete</button>
</div>
</details>

Danger Menu Item

Menu with destructive action

File ▾

<details s-menu>
<summary s-menu-trigger s-btn="outline">File ▾</summary>
<div s-menu-items>
  <button s-menu-item onclick="this.closest('details').open=false">Save</button>
  <button s-menu-item onclick="this.closest('details').open=false">Export</button>
  <hr s-menu-divider>
  <button s-menu-item s-danger onclick="this.closest('details').open=false">Delete</button>
</div>
</details>

Basic Items

<button s-menu-item>Edit</button>
<button s-menu-item>Duplicate</button>
<button s-menu-item>Delete</button>

Danger Item

For destructive actions:

<button s-menu-item s-danger>Delete permanently</button>

Disabled Item

<button s-menu-item disabled>Cannot edit</button>

With Icons

<button s-menu-item>
  <svg><!-- icon --></svg>
  Edit
</button>

Auto-Flip Behavior

In browsers that support CSS Anchor Positioning (Chrome 131+, Firefox 147+), menus automatically flip when they would overflow the viewport:

  • Bottom menu near viewport bottom → flips to top
  • End-aligned menu near right edge → flips to start
  • Combinations work too (bottom-end → top-start)

This is handled by position-try-fallbacks: flip-block, flip-inline with no JavaScript required.

Close on Item Click

Menus stay open after clicking an item (native <details> behavior). To close on click:

document.querySelectorAll('[s-menu-item]').forEach(item => {
  item.addEventListener('click', () => {
    item.closest('details[s-menu]').open = false;
  });
});

Accessibility

The native <details>/<summary> provides built-in accessibility:

  • Keyboard support: Enter/Space toggles menu
  • Screen reader support: Announces expanded/collapsed state
  • Focus management: Summary is focusable by default

For enhanced accessibility, add ARIA attributes:

<details s-menu>
  <summary s-menu-trigger s-btn="secondary" aria-haspopup="menu">
    Options ▾
  </summary>
  <div s-menu-items role="menu">
    <button s-menu-item role="menuitem">Edit</button>
    <button s-menu-item role="menuitem">Delete</button>
  </div>
</details>

Browser Support

FeatureChromeFirefoxSafari
Basic menuAllAllAll
Auto-flip131+147+No

Progressive Enhancement: The menu works everywhere using absolute positioning. Browsers with anchor-scope support get automatic viewport edge detection and flipping.

How It Works

The menu uses anchor-scope to isolate anchor names per menu instance:

[s-menu] {
  position: relative;
  anchor-scope: --menu-trigger;
}

[s-menu-trigger] {
  anchor-name: --menu-trigger;
}

/* Fallback for all browsers */
[s-menu-items] {
  position: absolute;
  top: 100%;
  left: 0;
}

/* Progressive enhancement */
@supports (anchor-scope: all) {
  [s-menu-items] {
    position: absolute;
    position-anchor: --menu-trigger;
    inset-block-start: anchor(end);
    inset-inline-start: anchor(start);
    position-try-fallbacks: flip-block, flip-inline;
  }
}

CSS Custom Properties

PropertyDescription
--_menu-bgBackground color
--_menu-borderBorder color
--_menu-radiusBorder radius
--_menu-shadowBox shadow
--_menu-min-widthMinimum width (default: 10rem)
--_menu-paddingInternal padding
--_menu-offsetGap between trigger and menu

All Attributes

AttributeElementPurpose
s-menudetailsMenu wrapper (values: end, top, top-end)
s-menu-triggersummaryMenu trigger button
s-menu-itemsdivMenu content container
s-menu-itembuttonIndividual menu action
s-menu-dividerhrVisual separator
s-menu-labelspanSection label
s-dangerMenu itemDestructive action styling

Customization Examples

Custom Width

[s-menu-items] {
  --_menu-min-width: 15rem;
}

Custom Colors

[s-menu-items] {
  --_menu-bg: var(--s-primary-900);
  --_menu-border: var(--s-primary-700);
}

Remove Shadow

[s-menu-items] {
  --_menu-shadow: none;
  --_menu-border: var(--s-border-default);
}

Compact Menu

[s-menu-items] {
  --_menu-padding: 0;
}

[s-menu-item] {
  padding: var(--s-space-1) var(--s-space-2);
  font-size: var(--s-text-xs);
}
Search