SortMenu

Let users pick the field and direction a collection is sorted by.

Installation

  1. Install the package.
npm install @hashintel/ds-components
  1. Import the component.
import { SortMenu } from "@hashintel/ds-components";

export const EntitySort = () => (
  <SortMenu
    items={[
      { name: "Name", sortKey: "name" },
      { name: "Created at", sortKey: "createdAt" },
    ]}
    onChange={(sortKey, direction) => {
      // re-query the collection with the new sort
    }}
  />
);

When to use

  • Do: Use SortMenu above lists, grids and card collections where column-header sorting isn't available.
  • Do: Use one SortMenu per collection, placed in the same toolbar as its filters.

When not to use

  • Don't: Don't add a SortMenu to a table whose column headers already sort — one control should own the order.
  • Don't: Don't offer sorting on short static lists users can scan at a glance — order them sensibly instead.

Examples

Basic

Pass the sorters as items and control the selection with value/onChange. Selecting a row applies it with its current direction; re-selecting the active sort unchanged just closes the menu.

Direction availability

Each sorter declares directionsAvailable: "both" (the default), "ascending" or "descending" for one-way fields, or "none" for fields like relevance where direction is meaningless — those rows render no direction icon or toggle.

Sort icons

sortIcon picks the glyph family for a sorter's direction icons: alphabetical (the default), numeric for counts and scores, and generic bars for everything else.

Icon-only trigger

renderTrigger="icon" collapses the trigger to the sort glyph alone for dense toolbars; the accessible name still reads the active sort.

Custom trigger

Pass a function to render a fully custom trigger; it receives the active sorter and direction. You are then responsible for the trigger's label and accessible name.

import { useState } from "react";

import { Button, SortMenu, type SortDirection } from "@hashintel/ds-components";

type SortKey = "name" | "createdAt";

export const EntitySort = () => {
  const [sort, setSort] = useState<{
    sortKey: SortKey;
    direction: SortDirection;
  }>({ sortKey: "name", direction: "ASCENDING" });

  return (
    <SortMenu<SortKey>
      items={[
        { name: "Name", sortKey: "name" },
        { name: "Created at", sortKey: "createdAt" },
      ]}
      value={sort}
      onChange={(sortKey, direction) => setSort({ sortKey, direction })}
      renderTrigger={(sorter, direction) => (
        <Button variant="subtle" iconName="sortDown">
          {sorter ? `Sorted by ${sorter.name}` : "Sort"}
          {direction === "DESCENDING" ? " (desc)" : ""}
        </Button>
      )}
    />
  );
};

Trigger appearance

The trigger is a Button and SortMenu forwards its props: variant (default ghost), tone and size (default sm). align="right" moves the sort glyph to the right of the label.

Searchable

searchable adds a search field above the options — reach for it once a menu passes roughly 10 sorters. The search clears whenever the menu closes.

Persisting the choice

saveSortId writes each change to localStorage and restores the saved sort on mount through onChange — only while value is still undefined, so state you restore yourself always wins. A saved sort naming a removed sorter or an unavailable direction is ignored.

import { useState } from "react";

import { SortMenu, type SortDirection } from "@hashintel/ds-components";

type SortKey = "name" | "createdAt";

export const EntitySort = () => {
  const [sort, setSort] = useState<
    { sortKey: SortKey; direction: SortDirection } | undefined
  >(undefined);

  return (
    <SortMenu<SortKey>
      items={[
        { name: "Name", sortKey: "name" },
        { name: "Created at", sortKey: "createdAt" },
      ]}
      value={sort}
      onChange={(sortKey, direction) => setSort({ sortKey, direction })}
      saveSortId="entities-table"
    />
  );
};

Behavior

  • Persistence via saveSortId is best-effort: storage errors are swallowed, and the restore runs at most once per mount.

Content

  • Name sorters after the field, 1–3 words in sentence case: "Name", "Created at", "Entity count". The name doubles as the trigger label, so keep it short.
  • Give every sorter a distinct name — the search field and accessible names match on it.

Accessibility

SortMenu implements the WAI-ARIA menu button pattern through Menu.

  • The trigger's accessible name reads the active sort — "Sort by Name, ascending" — or "Sort" when nothing is selected.
  • Direction toggles stay out of the tab order and their labels name the outcome of activating them ("Sort by Name, descending").

Keyboard

KeyAction
← / →Flip and apply the highlighted sorter's direction (bidirectional sorters only)

What you must do

  • With a custom renderTrigger, provide the trigger's accessible name yourself and keep it in step with the active sort.
  • Pick saveSortId values unique per collection — saved sorts are keyed by it globally.

API reference

SortMenu

PropTypeDescription
items
readonly Sorter<SortKey>[]
default: []
value
{ sortKey: NoInfer<SortKey>; direction: SortDirection; }
onChange
((sortKey: NoInfer<SortKey>, direction: SortDirection) => void)
saveSortId
string |⁠ null
searchable
boolean
default: false
align
"left" |⁠ "right"
default: "left"
position
Position
default: "bottom-start"
renderTrigger
"default" |⁠ "icon" |⁠ ((sorter: Sorter<NoInfer<SortKey>>, direction: SortDirection) => ReactElement<unknown, string |⁠ JSXElementConstructor<...>>) |⁠ undefined
default: "default"
shape
"round" |⁠ "default"
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "sm"
tone
"neutral" |⁠ "brand" |⁠ "error"
aria-label
string
tooltip
string
iconName
"function" |⁠ "filter" |⁠ "at" |⁠ "circle" |⁠ … 137 more
iconPosition
"left" |⁠ "right"
prefix
ReactNode
suffix
ReactNode
variant
"link" |⁠ "solid" |⁠ "subtle" |⁠ "ghost" |⁠ "linkSubtle"
default: "ghost"
loading
boolean
disabled
boolean
tabIndex
number
autoFocus
boolean |⁠ "never"
onMouseDown
MouseEventHandler<HTMLButtonElement>
onMouseUp
MouseEventHandler<HTMLButtonElement>
onMouseEnter
MouseEventHandler<HTMLButtonElement>
onMouseLeave
MouseEventHandler<HTMLButtonElement>
onKeyDown
KeyboardEventHandler<HTMLButtonElement>
onFocus
FocusEventHandler<HTMLButtonElement>
onBlur
FocusEventHandler<HTMLButtonElement>
tooltipOptions
Omit<BaseTooltipProps & { variant?: "light" |⁠ "dark"; }, "children" |⁠ "content">

Sorter

The shape of items entries — one sortable field each.

PropTypeDescription
name
string
sortKey
string
directionsAvailable
"none" |⁠ "both" |⁠ "ascending" |⁠ "descending"
default: "both"
sortIcon
"numeric" |⁠ "alphabetical" |⁠ "generic"
default: "alphabetical"

Was this page helpful?

On this page