Klarhimmel UI

Context menu

Stable

The menu that opens where you right-click, and from the keyboard with the Menu key or Shift+F10.

npx shadcn add @klarhimmel/context-menu @klarhimmel/menu

When to use

  • Use for

    Row, tile and canvas actions for mouse and keyboard users.

  • Not for

    The only route to an action. Touch has no right-click: repeat the actions in a visible button.

  • Pairs with

    Menu, Icon button, tables and file grids.

Preview

Booking 4821

Right-click here, or focus it and press the Menu key or Shift+F10.

Nothing chosen yet.

context-menu.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { ContextMenu } from '@/components/klarhimmel/ui/context-menu';
import { MenuItem } from '@/components/klarhimmel/ui/menu';

export default function Example() {
  return (
    <Theme theme="brand">
      <ContextMenu
        aria-label="Booking 4821"
        menu={<>
          <MenuItem>Open booking</MenuItem>
          <MenuItem danger>Cancel booking</MenuItem>
        </>}
      >
        Booking 4821
      </ContextMenu>
    </Theme>
  );
}

Install

Import

import { ContextMenu } from '@/components/klarhimmel/ui/context-menu';
Lands in
components/klarhimmel/ui/context-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

  • Rows

    • Maja Sund, room 204
    • Oskar Lind, room 311
    • Ida Nord, room 118

    Right-click a row. The button on each row has the same actions, for touch screens.

    import { useState } from 'react';
    import { MoreHorizontal } from 'lucide-react';
    import { ContextMenu } from '@/components/klarhimmel/ui/context-menu';
    import { IconButton } from '@/components/klarhimmel/ui/icon-button';
    import { Menu, MenuContent, MenuItem, MenuTrigger } from '@/components/klarhimmel/ui/menu';
    
    const rows = ['Maja Sund, room 204', 'Oskar Lind, room 311', 'Ida Nord, room 118'];
    
    export default function ContextMenuRows() {
      const [last, setLast] = useState('');
      const items = (who: string) => (
        <>
          <MenuItem onSelect={() => setLast(`Check in ${who}`)}>Check in</MenuItem>
          <MenuItem onSelect={() => setLast(`Message ${who}`)}>Send a message</MenuItem>
        </>
      );
      return (
        <div className="flex min-h-72 w-[min(28rem,calc(100cqw-3rem))] flex-col gap-3">
          <ul className="divide-y divide-kh-line rounded-kh-lg border border-kh-line bg-kh-surface">
            {rows.map((r) => (
              <li key={r} className="flex items-center gap-2 pr-2">
                <ContextMenu aria-label={r} className="flex-1 px-4 py-3 text-kh-small" menu={items(r)}>{r}</ContextMenu>
                <Menu>
                  <MenuTrigger asChild>
                    <IconButton label={`Actions for ${r}`} variant="ghost"><MoreHorizontal aria-hidden className="size-4" /></IconButton>
                  </MenuTrigger>
                  <MenuContent align="end">{items(r)}</MenuContent>
                </Menu>
              </li>
            ))}
          </ul>
          <p role="status" className="text-kh-small text-kh-muted">{last || 'Right-click a row. The button on each row has the same actions, for touch screens.'}</p>
        </div>
      );
    }
  • Tiles

    • rates-autumn.csv
    • house-rules.pdf
    • floor-plan.png

    Tab to a tile and press Shift+F10.

    import { useState } from 'react';
    import { ContextMenu } from '@/components/klarhimmel/ui/context-menu';
    import { MenuItem, MenuLabel, MenuSeparator } from '@/components/klarhimmel/ui/menu';
    
    const files = ['rates-autumn.csv', 'house-rules.pdf', 'floor-plan.png'];
    
    export default function ContextMenuTiles() {
      const [last, setLast] = useState('');
      return (
        <div className="flex min-h-72 w-[min(30rem,calc(100cqw-3rem))] flex-col gap-3">
          <ul className="grid grid-cols-3 gap-3">
            {files.map((f) => (
              <li key={f}>
                <ContextMenu
                  aria-label={f}
                  className="flex h-24 items-end rounded-kh-lg border border-kh-line bg-kh-surface-2 p-2 text-kh-caption [overflow-wrap:anywhere]"
                  menu={<>
                    <MenuLabel>{f}</MenuLabel>
                    <MenuSeparator />
                    <MenuItem onSelect={() => setLast(`Download ${f}`)}>Download</MenuItem>
                    <MenuItem onSelect={() => setLast(`Rename ${f}`)} shortcut="F2">Rename</MenuItem>
                  </>}
                >
                  {f}
                </ContextMenu>
              </li>
            ))}
          </ul>
          <p role="status" className="text-kh-small text-kh-muted">{last || 'Tab to a tile and press Shift+F10.'}</p>
        </div>
      );
    }

API reference

PropTypeDefaultDescription
aria-labelstring-Required: names the area, which is a tab stop.
menuReactNode-Required. MenuItem, MenuLabel, MenuSeparator and the other Menu parts.
childrenReactNode-What the area shows.
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
Right-clickOpens the menu at the pointer
Menu key / Shift+F10Opens it at the focused element
Down / Up / Home / EndMove through the items
LettersJump to an item that starts with them
EscapeCloses and returns focus to the area

It is the same Menu as a button's menu. The area is a named tab stop so keyboard users can reach it.

Accessibility