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/resetand@shift-css/core/tokensfor 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 ▾
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> Menu Items
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
Recommended Markup
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
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
| Basic menu | All | All | All |
| Auto-flip | 131+ | 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
| Property | Description |
|---|---|
--_menu-bg | Background color |
--_menu-border | Border color |
--_menu-radius | Border radius |
--_menu-shadow | Box shadow |
--_menu-min-width | Minimum width (default: 10rem) |
--_menu-padding | Internal padding |
--_menu-offset | Gap between trigger and menu |
All Attributes
| Attribute | Element | Purpose |
|---|---|---|
s-menu | details | Menu wrapper (values: end, top, top-end) |
s-menu-trigger | summary | Menu trigger button |
s-menu-items | div | Menu content container |
s-menu-item | button | Individual menu action |
s-menu-divider | hr | Visual separator |
s-menu-label | span | Section label |
s-danger | Menu item | Destructive 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);
}