Skip to content

Comments

[v10] Add option to make tables sortable#1654

Open
colinrotherham wants to merge 29 commits intosupport/10.xfrom
sortable-table
Open

[v10] Add option to make tables sortable#1654
colinrotherham wants to merge 29 commits intosupport/10.xfrom
sortable-table

Conversation

@colinrotherham
Copy link
Contributor

@colinrotherham colinrotherham commented Oct 30, 2025

Description

This expands the existing Table component to enable a new option to make some or all of the columns sortable by clicking the column header.

This is partly inspired by sortable table component from MOJ Frontend.

The table sorting feature is done using JavaScript as a progressive enhancement. If javascript is unavailable or not supported, then the table remains in its initial state, and no sort buttons are added.

It works by adding aria-sort attributes to the headers of columns that you want to be sortable.

The aria-sort attribute should be set to ascending or descending on the 1 column which is initially sorted, and none on all other columns that you want to be sortable:

{{ table({
  caption: "People",
  head: [
    { text: "Name", attributes: { "aria-sort": "ascending" },
    { text: "Age", attributes: { "aria-sort": "none" }
  ],
  rows: […]
}) }}

By default, the JavaScript will sort the column numerically if all cells contain a number, or will otherwise sort them alphabetically (or reverse-alphabetical).

If you need to specify an alternative sort order, you can do this by setting data-sort-value attributes on individual cells:

{{ table({
  caption: "Appointments",
  head: [
    { text: "Title", attributes: { "aria-sort": "none" },
    { text: "Date", attributes: { "aria-sort": "ascending" }
  ],
  rows: [
    [{ text: "Vaccination"}, {text: "4 January 2026", attributes: { "data-sort-value: "2026-01-04"}],
    [{ text: "Health check"}, {text: "15 February 2026", attributes: { "data-sort-value: "2026-02-15"}],
  ]
}) }}

Screenshots

Screenshot 2026-01-12 at 16 19 22 Screenshot 2026-01-12 at 16 19 37

Checklist

@paulrobertlloyd
Copy link
Contributor

paulrobertlloyd commented Oct 30, 2025

Question of icons…

  • Is it useful having the double pointed arrow for unsorted columns (as opposed to no icon)
  • Should the arrows be more chevron shaped, to be consistent with iconography used elsewhere?

On the later point, when I was using sortable tables in my service, I used the following SVG:

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 27 27">
  <path fill="#212b32" d="m13 18.6-7-7.2c-.6-.5-.6-1.4 0-2 .5-.5 1.3-.5 2 0l6 6.2 6-6.2c.7-.5 1.5-.5 2 0 .6.6.6 1.5 0 2l-7 7.2c-.3.3-.6.4-1 .4s-.7-.1-1-.4Z"/>
</svg>

@colinrotherham colinrotherham temporarily deployed to nhsuk-frontend-pr-1654 November 20, 2025 14:00 Inactive
@colinrotherham colinrotherham added table javascript Pull requests that update Javascript code labels Nov 20, 2025
@anandamaryon1 anandamaryon1 temporarily deployed to nhsuk-frontend-pr-1654 November 25, 2025 16:55 Inactive
@anandamaryon1
Copy link
Contributor

anandamaryon1 commented Nov 25, 2025

Pushed some initial styling for the sortable tables heading buttons and icons, based on designs worked on in the sortable tables working group.

For discussion.

Some notes:

  • I've kept the icon to the left of the heading for right-aligned header cells, to avoid icons being placed right beside each other.
  • I've reduced the current sorted underline from 4px to 2px, to make it play nicer with the focus style, I'm open to re-exploring this. (it looked odd since the sorted underline indicator runs from bottom up, and the focus style runs from top down.)
  • Playing with it I notice that the first click makes the column ascending - showing smallest first, which makes sense with A-Z. But for some reason, I expect biggest first, with numbers when I click for an initial sort.

To do:

  • Give non-button headings matching padding as buttons. (currently only buttons have 2px padding on the outer edge)
  • Consider icon and spacing - ascending and descending arrows have inbuilt padding (in the SVG) which affects spacing between the icon and heading text. But, SVGs ideally should all be the same size to avoid content jumping around when swapping them out. Open to ideas.

Screenshot:

image image image
Preview it:

https://nhsuk-frontend-pr-1654.herokuapp.com/nhsuk-frontend/components/tables/with-numeric-data-sortable/

@colinrotherham
Copy link
Contributor Author

Can we take any inspiration from tabs where only the middle bit is focused?

Tab with focus

@anandamaryon1
Copy link
Contributor

Can we take any inspiration from tabs where only the middle bit is focused?

Tab with focus

That could make things a lot less hacky with styling the buttons. I'm fine with the focus being on the text only, but we do want the increased target area, and the hover should match, to show where you can click to activate it. Would that rely on JS click events for the table header?

@colinrotherham
Copy link
Contributor Author

Don't let the small target area from tabs put you off, both could be bigger

Expanders are similar where target area focus styles are removed, with (fake) focus styles on the text only:

Expander focus

…Would that rely on JS click events for the table header?

I'm assuming yes, but we'd need to research why click events on child elements weren't used

The current single-listener event bubbling approach could be for either compatibility or performance

@frankieroberto
Copy link
Contributor

Expanders are similar where target area focus styles are removed, with (fake) focus styles on the text only

Task list does the same. On the other hand the combined logo + service name link in the header has the focus style cover the whole clickable area, including the logo.

@paulrobertlloyd
Copy link
Contributor

Probably need an audit of focus styles… part of me wants to suggest making focus styles big and obvious, now that Firefox doesn’t show them on click (or can at least be avoided)

@anandamaryon1 anandamaryon1 temporarily deployed to nhsuk-frontend-pr-1654 December 4, 2025 14:46 Inactive
@anandamaryon1
Copy link
Contributor

Seems all the other components expand the click area by placing an absolutely positioned :after element to cover the parent container. This is tricky with the table header buttons because of display: table-cell (hence my earlier hacky CSS to grow the buttons).

I've now removed that so that the focus is placed only on the natural sized buttons, and added a hover state to the header cells to illustrate how I'd like them to be, but of course this doesn't trigger the buttons.

Can someone help me add a click event to the table heads? Or maybe there's a better way?

I also notice a possible slight bug where the aria-sort="none" is added to none sortable columns when one is sorted, but maybe this is on purpose to identify the state of the columns?

@colinrotherham
Copy link
Contributor Author

Can someone help me add a click event to the table heads? Or maybe there's a better way?

There's an event listener on the <head> element you can use

this.$head.addEventListener('click', this.onSortButtonClick.bind(this))

But later on you'll see it ignores all clicks unless it bubbled up from a button element

onSortButtonClick(event) {
  const $target = /** @type {HTMLElement} */ (event.target)
  const $button = $target.closest('button')

  if (!$button?.parentElement) {
    return
  }
  
  // …
 }

In theory you could change this code to allow clicks from non-clickable things

But we shouldn't do really

Is it a bad thing that only the <button> is clickable, like MOJ do?

@colinrotherham
Copy link
Contributor Author

Rebased with main to fix the conflict

Table examples will now be in table/macro-options.mjs table/fixtures.mjs

@frankieroberto
Copy link
Contributor

Can someone help me add a click event to the table heads? Or maybe there's a better way?

Is it a bad thing that only the <button> is clickable, like MOJ do?

The MOJ one doesn't have any hover state. We’ve added a hover state (background colour change to grey) for the whole <thead>. The clickable area should be match the hover state area, I think – like on the Task list rows. And in general, the bigger the hit area the better?

Would one option be to make button size expand to fill the entirety of the <thead>, and then move the background hover colour to the <button>?

I also just spotted that the task list rows are using color.adjust(nhsuk-colour("grey-5"), $lightness: -6%); whereas the sortable table headers are using nhsuk-colour("grey-4") – should they be consistent? The task list one has more of a noticeable blue tint to me, and isn’t quite as dark.

@anandamaryon1
Copy link
Contributor

Can someone help me add a click event to the table heads? Or maybe there's a better way?

Is it a bad thing that only the <button> is clickable, like MOJ do?
Would one option be to make button size expand to fill the entirety of the <thead>, and then move the background hover colour to the <button>?

This is what I had first, which kinda worked but relied on some hacky CSS to get the button to grow to fill the <thead>. And meant the focus state applied to the whole area (screens here).

I'm not sure on the best approach from a frontend dev perspective but what I want to achieve is the whole <thead> clickable, with a hover state that matches the clickable area. And a focus state that applies only to the text if possible, to better match other components, eg. tabs.

I also just spotted that the task list rows are using color.adjust(nhsuk-colour("grey-5"), $lightness: -6%); whereas the sortable table headers are using nhsuk-colour("grey-4") – should they be consistent? The task list one has more of a noticeable blue tint to me, and isn’t quite as dark.

Ohh, well spotted, will need to take a look at this. I think I was using pagination as my reference.

@frankieroberto
Copy link
Contributor

@anandamaryon1 the task list trick is to use this to generate a bigger click area, but then the focus background colour only applies to the smaller <a> element:

.nhsuk-task-list__link::after {
  content: "";
  display: block;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

Bit of a hack but it seems to work?! There might be a better way though.

@colinrotherham
Copy link
Contributor Author

Sorry for the rebase, but I've updated this branch to pick up the new status check names

@colinrotherham colinrotherham changed the title Add option to make tables sortable [v10] Add option to make tables sortable Feb 17, 2026
@colinrotherham colinrotherham changed the base branch from main to support/10.x February 17, 2026 15:54
@colinrotherham colinrotherham temporarily deployed to nhsuk-frontend-pr-1654 February 17, 2026 15:55 Inactive
@colinrotherham
Copy link
Contributor Author

colinrotherham commented Feb 17, 2026

@frankieroberto @anandamaryon1 I've updated this PR to target v10.x

Can you please check the "link" colours and update if needed?

Good to get this out sooner rather than later

Sortable table link styles

@colinrotherham colinrotherham temporarily deployed to nhsuk-frontend-pr-1654 February 17, 2026 16:03 Inactive
@sonarqubecloud
Copy link

@anandamaryon1
Copy link
Contributor

Yep the link colours are OK for now (pre-v11). I don't think we need to change the "active" border-bottom colour, it can stay blue.

We're doing pilot testing and UR on this over the next few weeks, so can't say yet whether it'll be v10 or v11 when it's ready to release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code table

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants