Decide what sorting means before writing the comparator
A useful sortable list answers two questions: what property is being sorted, and which direction is active? For a list of names, “A to Z” and “Z to A” are clearer control labels than “ascending” and “descending.” If you later add sorting by price or date, name the property as well. Keep the original data separate from the displayed order so a change in direction does not permanently rearrange your source array.
Equal values need a rule too. Consider the names “apple” and “Apple.” A case-insensitive comparison may treat them as equal. If their relative positions change whenever someone switches direction, the list can feel unpredictable. Give each item an original position and use it to break ties. Apply the direction only to the name comparison; applying it to the tie-breaker would reverse equal items as well.
Build a small browser example
The following example runs in a modern browser console on an ordinary page. It creates its own heading, controls, status message, and ordered list, so it does not require a framework or existing markup. It uses Intl.Collator for name comparisons and Array.prototype.sort on a copy of the data. The original index provides an explicit tie-breaker rather than relying on the sorting implementation to preserve ties.
const names = ["Banana", "apple", "Cherry", "Apple"]; const items = names.map((name, index) => ({ name, index })); const collator = new Intl.Collator(undefined, { sensitivity: "base" }); let direction = "asc"; const container = document.createElement("section"); const heading = document.createElement("h2"); heading.textContent = "Items by name"; const controls = document.createElement("div"); controls.setAttribute("role", "group"); controls.setAttribute("aria-label", "Sort items by name"); const ascendingButton = document.createElement("button"); ascendingButton.type = "button"; ascendingButton.textContent = "Sort A to Z"; const descendingButton = document.createElement("button"); descendingButton.type = "button"; descendingButton.textContent = "Sort Z to A"; controls.append(ascendingButton, descendingButton); const status = document.createElement("p"); status.setAttribute("role", "status"); const list = document.createElement("ol"); container.append(heading, controls, status, list); document.body.append(container); function render() { const multiplier = direction === "asc" ? 1 : -1; const ordered = items.slice().sort((a, b) => { const byName = collator.compare(a.name, b.name); return byName === 0 ? a.index - b.index : byName * multiplier; }); const rows = ordered.map(({ name }) => { const row = document.createElement("li"); row.textContent = name; return row; }); list.replaceChildren(...rows); ascendingButton.setAttribute("aria-pressed", String(direction === "asc")); descendingButton.setAttribute("aria-pressed", String(direction === "desc")); status.textContent = direction === "asc" ? "Sorted A to Z" : "Sorted Z to A"; } ascendingButton.addEventListener("click", () => { direction = "asc"; render(); }); descendingButton.addEventListener("click", () => { direction = "desc"; render(); }); render();
Keep direction visible and usable
The two buttons act as a choice of direction. Their labels describe the action, while aria-pressed identifies the active choice. The visible status text states the result and, because it has the status role, can announce an update to screen-reader users without moving focus. Native buttons also provide keyboard operation without custom key handlers. The example replaces only list items during a sort, leaving the controls in place so keyboard focus stays on the button that was used.
For this sample, the ascending order is “apple,” “Apple,” “Banana,” “Cherry.” Descending puts “Cherry” first, but “apple” still precedes “Apple” because their name comparison is equal and their original positions break the tie. Test both buttons with a keyboard and check the displayed order, pressed state, and status text. If you adapt the example for dates or numbers, replace the name comparator with a comparison appropriate to that data; keep the explicit tie-breaker.
Adapt the pattern to real component data
In a component, treat the source items and selected direction as separate state. Derive the ordered view from them instead of repeatedly sorting whatever order happens to be on screen. Use a persistent item identifier when rendering rows, particularly if rows contain interactive controls; an array position in the displayed order is not a reliable identity. If the data is loaded or edited later, decide whether ties should follow initial arrival order or another stable business rule, then store the value needed to enforce that rule.
For longer lists, consider whether the controls and status remain understandable after other filters are applied. A message such as “Sorted A to Z” explains direction but does not say whether the list is filtered. Keep each message accurate to the state the user can see. The core pattern remains small: preserve the source data, compare the chosen field, resolve ties consistently, and make the active direction apparent without relying on visual styling alone.



