Menu

A list of actions that opens under a button (also called a dropdown menu). Reach for it when a place has more actions than room for buttons, or for the actions of each row in a list.

import { Menu, MenuItem, MenuSeparator } from "@umriss-ui/core";

The entries are things that happen, not values that stay; choosing one runs it and closes the menu so the result is in view.

An IconButton makes the trigger for a row; its label says what it acts on, such as “Actions for INC-1048”.

Examples

Run actions from a trigger

Give trigger a button and the entries as MenuItem; each runs its onSelect and the menu closes.

import { useState } from "react";
import { Button, Menu, MenuItem, MenuSeparator, Stack, Text } from "@umriss-ui/core";

export default function Actions() {
  const [last, setLast] = useState("-");

  return (
    <Stack gap={3} align="flex-start">
      <Menu trigger={<Button size="sm">Actions</Button>}>
        <MenuItem onSelect={() => setLast("Exported as CSV")}>Export as CSV</MenuItem>
        <MenuItem onSelect={() => setLast("Link copied")}>Copy the link</MenuItem>
        <MenuSeparator />
        <MenuItem tone="danger" onSelect={() => setLast("View removed")}>
          Remove the view
        </MenuItem>
      </Menu>
      <Text size="xs" tone="muted">
        Last: {last}
      </Text>
    </Stack>
  );
}

Align with the right edge

For a trigger at the right of a header, align set to end lines the panel up with the trigger's right edge.

import { Button, Menu, MenuItem, Stack, Text } from "@umriss-ui/core";

export default function AlignEnd() {
  return (
    <Stack direction="row" justify="space-between" align="center" style={{ maxWidth: 520 }}>
      <Text weight="medium">Budget 2026 · Marketing</Text>
      <Menu align="end" trigger={<Button size="sm" variant="ghost">View</Button>}>
        <MenuItem>Show forecast</MenuItem>
        <MenuItem>Show last year</MenuItem>
        <MenuItem>Show in thousands</MenuItem>
      </Menu>
    </Stack>
  );
}

Disable an entry, mark a destructive one

A disabled entry keeps its place and is skipped by the arrow keys; the danger tone marks what destroys, after a separator.

import { Button, Menu, MenuItem, MenuSeparator } from "@umriss-ui/core";

export default function DisabledAndDanger() {
  return (
    <Menu trigger={<Button size="sm">Invoice INV-26-0311</Button>}>
      <MenuItem>Open</MenuItem>
      <MenuItem disabled>Approve (already paid)</MenuItem>
      <MenuItem>Download as PDF</MenuItem>
      <MenuSeparator />
      <MenuItem tone="danger">Cancel the invoice</MenuItem>
    </Menu>
  );
}

Act on a row

One menu per row keeps a list quiet; the trigger's name says which row it acts on, since every button reads the same.

import { useState } from "react";
import { Badge, IconButton, Menu, MenuItem, MenuSeparator, Stack, Text } from "@umriss-ui/core";

interface Incident {
  id: string;
  title: string;
  severity: "SEV1" | "SEV2" | "SEV3";
  state: "open" | "acknowledged" | "resolved";
}

const INCIDENTS: Incident[] = [
  { id: "INC-1048", title: "Checkout slow, card payments time out", severity: "SEV1", state: "acknowledged" },
  { id: "INC-1047", title: "Webhook deliveries delayed", severity: "SEV3", state: "open" },
  { id: "INC-1046", title: "Thumbnails missing for new uploads", severity: "SEV2", state: "resolved" },
];

const TONE = { SEV1: "danger", SEV2: "warning", SEV3: "neutral" } as const;

export default function PerRow() {
  const [incidents, setIncidents] = useState(INCIDENTS);
  const set = (id: string, state: Incident["state"]) =>
    setIncidents((all) => all.map((one) => (one.id === id ? { ...one, state } : one)));

  return (
    <Stack gap={2} style={{ maxWidth: 620 }}>
      {incidents.map((incident) => (
        <Stack key={incident.id} direction="row" gap={3} align="center">
          <Badge tone={TONE[incident.severity]}>{incident.severity}</Badge>
          <Text size="sm" style={{ flex: 1 }}>
            <Text as="span" size="sm" mono style={{ whiteSpace: "nowrap" }}>
              {incident.id}
            </Text>{" "}
            {incident.title}
          </Text>
          <Text as="span" size="xs" tone="muted">
            {incident.state}
          </Text>
          <Menu
            align="end"
            trigger={
              <IconButton size="sm" aria-label={`Actions for ${incident.id}`}>
                ⋯
              </IconButton>
            }
          >
            <MenuItem disabled={incident.state !== "open"} onSelect={() => set(incident.id, "acknowledged")}>
              Acknowledge
            </MenuItem>
            <MenuItem disabled={incident.state === "resolved"} onSelect={() => set(incident.id, "resolved")}>
              Resolve
            </MenuItem>
            <MenuItem>Copy the link</MenuItem>
            <MenuSeparator />
            <MenuItem tone="danger" onSelect={() => setIncidents((all) => all.filter((one) => one.id !== incident.id))}>
              Delete
            </MenuItem>
          </Menu>
        </Stack>
      ))}
    </Stack>
  );
}

When to use something else

Keyboard

Key Action
Enter or Space On the trigger, opens the menu and moves focus to the first entry; on an entry, runs it and closes the menu.
↓ / ↑ Moves to the next or previous entry, wrapping at the ends; disabled entries are skipped.
Home / End Moves to the first or last entry.
Escape Closes the menu and returns focus to the trigger.
Tab Closes the menu and moves on from the trigger.

API

MenuProps

NameTypeDefaultDescription

MenuItemProps

NameTypeDefaultDescription

Events

NameTypeDefaultDescription

Also takes every attribute of <button> – without onSelect.

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index