SortMenu
Let users pick the field and direction a collection is sorted by.
Installation
- Install the package.
npm install @hashintel/ds-components- 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
saveSortIdis 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
| Key | Action |
|---|---|
| ← / → | 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
saveSortIdvalues unique per collection — saved sorts are keyed by it globally.
API reference
SortMenu
| Prop | Type | Description |
|---|---|---|
| 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 | Adds a search bar to the top of the dropdown that filters the sorters |
| align | "left" | "right" default: "left" | Which side of the trigger label the sort icon sits on. Defaults to left. |
| position | Position default: "bottom-start" | Dropdown placement relative to the trigger. Defaults to bottom-start. |
| renderTrigger | "default" | "icon" | ((sorter: Sorter<NoInfer<SortKey>>, direction: SortDirection) => ReactElement<unknown, string | JSXElementConstructor<...>>) | undefined default: "default" | "default" labels the trigger with the active sorter's name; "icon" collapses it to an icon-only button. A function renders a fully custom trigger |
| shape | "round" | "default" | The shape of the button. Non default shapes should very rarely be used |
| size | "xxs" | "xs" | "sm" | "md" | "lg" default: "sm" | The size (height) of the button |
| tone | "neutral" | "brand" | "error" | The color treatment of the button, e.g. for primary or destructive actions. |
| aria-label | string | Accessible name; required when there is no label or tooltip. Defines a string value that labels the current element. @see aria-labelledby. |
| tooltip | string | Tooltip shown on hover/focus; also names icon-only buttons |
| iconName | "function" | "filter" | "at" | "circle" | … 137 more | Optional icon to display |
| iconPosition | "left" | "right" | Which side of the label the icon is rendered on. |
| prefix | ReactNode | Element rendered at the start of the button. |
| suffix | ReactNode | Element rendered at the end of the button. |
| variant | "link" | "solid" | "subtle" | "ghost" | "linkSubtle" default: "ghost" | The overall visual style, from solid fill to inline link text; subtle and ghost sit between with a tinted or transparent background. |
| loading | boolean | Whether the button is in a loading state |
| disabled | boolean | |
| tabIndex | number | |
| autoFocus | boolean | "never" | Whether to focus the element on mount; "never" prevents the element being focused automatically. |
| 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"> | Options forwarded to the tooltip shown when tooltip is set |
Sorter
The shape of items entries — one sortable field each.
| Prop | Type | Description |
|---|---|---|
| name | string | Visible label of the sorter in the dropdown — also what the search bar matches, and the trigger label while this sorter is active |
| sortKey | string | Identifies the sorter in value and onChange |
| directionsAvailable | "none" | "both" | "ascending" | "descending" default: "both" | Which sort directions the sorter offers. Defaults to "both" |
| sortIcon | "numeric" | "alphabetical" | "generic" default: "alphabetical" | Glyph family for the direction icons. Defaults to "alphabetical" |
Was this page helpful?