- Components
Menu
A list of actions behind a button. Arrow keys move, type-ahead jumps. Built on Radix Dropdown Menu.
npx shadcn add @klarhimmel/menu @klarhimmel/buttonWhen to use
Use for
Three or more actions on one thing: rename, duplicate, delete.
Not for
Navigation between pages, or picking a value. Use links or a select.
Pairs with
Button or Icon button as the trigger, Kbd hints.
Preview
menu.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { Button } from '@/components/klarhimmel/ui/button';
import { Menu, MenuTrigger, MenuContent, MenuItem, MenuSeparator } from '@/components/klarhimmel/ui/menu';
export default function Example() {
return (
<Theme theme="brand">
<Menu>
<MenuTrigger asChild><Button variant="secondary">Actions</Button></MenuTrigger>
<MenuContent>
<MenuItem onSelect={() => console.log('rename')}>Rename</MenuItem>
<MenuSeparator />
<MenuItem danger onSelect={() => console.log('delete')}>Delete</MenuItem>
</MenuContent>
</Menu>
</Theme>
);
}Install
Import
import { Menu, MenuTrigger, MenuContent, MenuItem, MenuCheckItem, MenuRadioGroup, MenuRadioItem, MenuLabel, MenuSeparator } from '@/components/klarhimmel/ui/menu';- Lands in
- components/klarhimmel/ui/menu.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
- Render in
- Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.
- Limit
- Overlays render into the Theme root with position: fixed. A transform on an ancestor re-parents fixed (a dialog centres in the wrapper) and overflow: hidden clips menus. Render <Theme> at the app root, outside any transformed wrapper.
Examples
Options
Showing: Guest, Room, sorted by guest.
import { useState } from 'react'; import { ChevronDown } from 'lucide-react'; import { Button } from '@/components/klarhimmel/ui/button'; import { Menu, MenuCheckItem, MenuContent, MenuLabel, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuTrigger } from '@/components/klarhimmel/ui/menu'; const columns = ['Guest', 'Room', 'Arrival', 'Price']; export default function MenuOptions() { const [sort, setSort] = useState('guest'); const [shown, setShown] = useState<string[]>(['Guest', 'Room']); return ( <div className="flex min-h-12 flex-col items-start gap-3"> <Menu> <MenuTrigger asChild> <Button variant="secondary" iconEnd={<ChevronDown aria-hidden className="size-4" />}>Columns</Button> </MenuTrigger> <MenuContent> <MenuLabel>Show columns</MenuLabel> <MenuSeparator /> {columns.map((c) => ( <MenuCheckItem key={c} checked={shown.includes(c)} onCheckedChange={(on) => setShown((s) => (on ? [...s, c] : s.filter((x) => x !== c)))}> {c} </MenuCheckItem> ))} <MenuSeparator /> <MenuLabel>Sort by</MenuLabel> <MenuRadioGroup value={sort} onValueChange={setSort}> <MenuRadioItem value="guest">Guest</MenuRadioItem> <MenuRadioItem value="arrival">Arrival</MenuRadioItem> </MenuRadioGroup> </MenuContent> </Menu> <p aria-live="polite" className="text-kh-small text-kh-muted">Showing: {shown.join(', ') || 'nothing'}, sorted by {sort}.</p> </div> ); }Row actions
- Seaside suite
- Garden room
- Attic double
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react'; import { IconButton } from '@/components/klarhimmel/ui/icon-button'; import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@/components/klarhimmel/ui/menu'; const rooms = ['Seaside suite', 'Garden room', 'Attic double']; export default function MenuRowActions() { return ( <ul className="flex w-full max-w-md flex-col divide-y divide-kh-line rounded-kh border border-kh-line bg-kh-surface"> {rooms.map((room) => ( <li key={room} className="flex items-center justify-between gap-3 py-1 pl-4 pr-2 text-kh-small"> {room} <Menu> <MenuTrigger asChild> <IconButton label={`Actions for ${room}`} noTooltip size="sm"><MoreHorizontal aria-hidden className="size-5" /></IconButton> </MenuTrigger> <MenuContent> <MenuItem icon={<Pencil />}>Rename</MenuItem> <MenuSeparator /> <MenuItem icon={<Trash2 />} danger>Delete</MenuItem> </MenuContent> </Menu> </li> ))} </ul> ); }
Anatomy
The parts and how they nest.
Menu
├─ MenuTrigger (asChild)
└─ MenuContent
├─ MenuLabel
├─ MenuItem / MenuCheckItem
└─ MenuSeparatorMenuHolds the open state.MenuTriggerWrap your Button with asChild.MenuContentThe floating list. Portals into the theme root so it keeps the tokens.MenuItemAn action. Takes icon, shortcut, danger and onSelect.MenuCheckItemA toggle that stays in the menu.MenuLabel, MenuSeparatorA group heading and a divider.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
MenuTrigger asChild | ReactNode | - | Wrap the Button that opens the menu. |
MenuContent align / side | 'start' | 'center' | 'end' / 'top' | 'right' | 'bottom' | 'left' | 'start' / 'bottom' | Where it opens; it flips to stay on screen. |
MenuItem onSelect | () => void | - | Runs on Enter, Space or click, then the menu closes. |
MenuItem icon / shortcut | ReactNode / string | - | A leading icon and a trailing shortcut hint. |
MenuItem danger / disabled | boolean | false | Destructive tint; disabled items are skipped by the arrow keys. |
MenuCheckItem checked / onCheckedChange | boolean / (checked: boolean) => void | - | An item that toggles. The menu stays open, so several can be changed in one visit. |
MenuRadioGroup, MenuRadioItem | string / (value: string) => void | - | value and onValueChange on the group, value on each item. A choice of one; picking an item keeps the menu open. |
MenuLabel, MenuSeparator | - | - | A group heading and a divider. |
Accessibility
| Key | Does |
|---|---|
Enter / Space / Down | Opens the menu on the trigger |
Up / Down | Moves between items |
Home / End | First and last item |
A letter | Jumps to the item that starts with it |
Enter / Space | Chooses the item and closes |
Tab | Closes the menu |
Escape | Closes and returns focus to the trigger |
Disabled items are skipped by the arrow keys. Tab closes the menu and returns focus to the trigger. Check and radio items keep it open.
Accessibility