ComponentsSelection

Display menu

One popover for how a View reads: Sort, Group and Fields, each opening its own page, then the shared View actions.

Playground

Change the controls to try each prop. Open Display in the preview to walk its pages; the code lists only the props you changed.

States
import { DisplayMenu } from '@/ui/display-menu';<DisplayMenu />

Sort, group and fields

Each row shows its current value in subtle text and opens a page. Sort carries a direction button once a field is chosen. Sort and Group list only the fields the server can sort or group; the chosen one takes the selected fill and a check. Fields lists shown columns in table order with drag handles, then Hidden; the name column has no handle and can't be hidden. Every change edits the table beside it at once.

CandidateCompanyStageUpdated
Maya ChenHalcyon BankCV sent2026-10-14
Priya ShahHalcyon BankCV sent2026-10-09
Daniel OkaforOrbital FreightPlaced2026-10-03
import { useState } from 'react';
import { type DisplayField, DisplayMenu } from '@/ui/display-menu';
import { GroupRow } from '@/ui/group-row';
import { Table, type TableColumn, type TableSort } from '@/ui/table';

type Candidate = { id: string; name: string; company: string; stage: string; updated: string; owner: string };

const candidates: Candidate[] = [
  { id: 'c1', name: 'Maya Chen', company: 'Halcyon Bank', stage: 'CV sent', updated: '2026-10-14', owner: 'Joseph Ariel' },
  { id: 'c2', name: 'Daniel Okafor', company: 'Orbital Freight', stage: 'Placed', updated: '2026-10-03', owner: 'Sara Lee' },
  { id: 'c3', name: 'Priya Shah', company: 'Halcyon Bank', stage: 'CV sent', updated: '2026-10-09', owner: 'Joseph Ariel' },
];

const fields: DisplayField[] = [
  { key: 'name', label: 'Candidate', kind: 'text', sortable: true, pinned: true },
  { key: 'company', label: 'Company', kind: 'company', sortable: true, groupable: true },
  { key: 'stage', label: 'Stage', kind: 'status', sortable: true, groupable: true },
  { key: 'updated', label: 'Updated', kind: 'timestamp', sortable: true },
  { key: 'owner', label: 'Owner', kind: 'person', sortable: true, groupable: true },
];

// Display edits the table beside it: sort, group and the columns. The example sorts and groups the rows itself.
export default function DisplayMenuDefault() {
  const [sort, setSort] = useState<TableSort | null>({ key: 'updated', direction: 'desc' });
  const [group, setGroup] = useState<string | null>(null);
  const [shown, setShown] = useState(['name', 'company', 'stage', 'updated']);
  const columns: TableColumn<Candidate>[] = shown.map((key) => ({ key, label: fields.find((field) => field.key === key)!.label, cell: (row: Candidate) => row[key as keyof Candidate] }));
  const rows = [...candidates].sort((a, b) => {
    const by = (key: keyof Candidate) => a[key].localeCompare(b[key]);
    const grouped = group ? by(group as keyof Candidate) : 0;
    if (grouped) return grouped;
    return sort ? (sort.direction === 'asc' ? 1 : -1) * by(sort.key as keyof Candidate) : 0;
  });
  return (
    <div className="flex w-full flex-col gap-100">
      <div className="flex justify-end">
        <DisplayMenu
          fields={fields}
          sort={sort}
          onSortChange={setSort}
          group={group}
          onGroupChange={setGroup}
          shown={shown}
          onShownChange={setShown}
          onSaveAsNew={() => {}}
          onRename={() => {}}
          onDelete={() => {}}
          restoreDefaults={[{ key: 'all', label: 'All candidates' }, { key: 'mine', label: 'Mine' }]}
          onRestore={() => {}}
        />
      </div>
      <Table
        label="Candidates"
        columns={columns}
        rows={rows}
        sort={sort}
        groupOf={group ? (row) => row[group as keyof Candidate] : undefined}
        groupRow={(item) => <GroupRow field={fields.find((field) => field.key === group)!.label} value={item.key} count={item.rows.length} span={item.span} />}
      />
    </div>
  );
}

Someone who isn't an Owner or Admin

Everyone can save as new and rename. Only Owners and Admins delete and restore, so those items don't show.

import { type DisplayField, DisplayMenu } from '@/ui/display-menu';

const fields: DisplayField[] = [
  { key: 'name', label: 'Company', kind: 'text', sortable: true, pinned: true },
  { key: 'sector', label: 'Sector', kind: 'tags' },
  { key: 'owner', label: 'Owner', kind: 'person', sortable: true, groupable: true },
];

// A Member saves as new and renames; only Owners and Admins delete and restore, so those items don't show.
export default function DisplayMenuMember() {
  return <DisplayMenu data-replay="" fields={fields} defaultShown={['name', 'sector', 'owner']} onSaveAsNew={() => {}} onRename={() => {}} />;
}

While a View saves

Every View item disables until the save ends. The only View can't be deleted, and Delete View says why in its tooltip.

import { type DisplayField, DisplayMenu } from '@/ui/display-menu';

const fields: DisplayField[] = [
  { key: 'name', label: 'Job', kind: 'text', sortable: true, pinned: true },
  { key: 'status', label: 'Status', kind: 'status', sortable: true, groupable: true },
];

// While a View save is pending every View item disables. The only View can't be deleted, and says why.
export default function DisplayMenuSaving() {
  return (
    <DisplayMenu
      fields={fields}
      defaultShown={['name', 'status']}
      busy
      onSaveAsNew={() => {}}
      onRename={() => {}}
      onDelete={() => {}}
      deleteDisabled="This is the only View. Save another View first."
    />
  );
}

On this page