shadcn.io is not affiliated with official shadcn/ui
Sort Options Dialog Block for React Aria
A data-grid tool's sort dialog shown open: up to three rules with a field and a direction each, Move up, Move down and Remove, a sorted five-row preview with the sorted columns numbered, and the table re-sorting on apply.
Installation
Overview
Let people say how a table is ordered, and what breaks a tie, with this sort dialog for React with React Aria. It opens from the Tasks page of Sortleaf, a data-grid tool: the page's header holds an outline Sort Button, and the table, a toolbar with a Sort chip, a head of bars and six ruled rows with real titles and dates, is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The Dialog lists up to three rules, each a Native Select for the field, Due date, Priority, Assignee or Title, a Segment Group for the direction with an arrow and a word, and Move up, Move down and Remove buttons. A five-row preview under them is sorted by the same stable comparator as you change a rule, its sorted columns weighted and numbered by level. Reset goes back to the page's own order, and the only Shine Button, Apply sort, re-sorts the drawn table and writes its chip, "Sort: Due date up, Priority down". For preview the dialog is drawn open in place when the page loads; the header button opens the real Dialog, with the same rules. For tables, task lists, inboxes and any list that has more than one way to be ordered.
FAQ
Why is the dialog already open, and what do I keep?
So you can see it without a click. On load the dialog is a copy drawn in place over the table: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Sort button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same SortBody component and share the block's state, so the rules you set in the copy are there in the real one. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.
How does the tie-break work?
The first rule decides the order, the second only orders rows the first leaves tied, and the third only those still tied after it. Rows that tie on every rule keep the order the page loaded them in, so the sort is stable and the same on every device. Move up and Move down change which rule counts first. With Due date up and then Priority down, the two tasks due on Oct 16 are ordered Urgent before Medium; the preview's numbered column heads show which rule decides what.
Why can't I pick the same field twice?
A second rule on the same field could never break a tie the first one left, so the fields a rule already uses are unavailable in the others, and Add a level offers the first field that is still free. Priority sorts by rank, Low then Medium then High then Urgent, not alphabetically; Assignee and Title use a locale-aware comparison; Due date compares real dates.
What does the preview show on a phone?
The first five of the six rows, sorted as the rules say. On a phone the Assignee column folds into the Task cell as a second line, so a rule on Assignee is still visible. The sorted columns are weighted in the head and the cells and carry a number and an arrow, never a colour alone.
What stays local in the demo, and how do I wire it up?
All of it. The six tasks are a typed list and Apply sort only re-sorts the drawn table in this tab and says so in a polite message. In your app send the rules, a list of field and direction, to your API or sort a query with them, and keep the comparator for rows already on the page; Apply says "unchanged" instead of re-running when nothing differs.
Is it accessible from the keyboard?
Yes. The real dialog is named by its title and described by its sentence, focus starts on the first field and stays inside until the dialog closes, and Escape, Cancel and the close button return focus to Sort. The rules are a labelled list of native selects and segment groups, each named for its field, and every move is said in a polite message. The button and the dialog sit inside DialogTrigger, controlled with isOpen and onOpenChange, React Aria focuses the dialog itself so a frame later focus moves to the first field, and the title is a level 2 heading. The copy shown on load moves no focus; Escape inside it, Cancel and its close button close it and put focus on the button.