`UTable`: no way to set arbitrary attributes on `<th>` (`meta.class.th` / `meta.style.th` exist, no attribute equivalent) — blocks `aria-sort`
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 6.9k
- Forks
- 1.1k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 57
Description
Description
UTable renders sortable column headers but never emits aria-sort on the <th> element. A sortable
column is announced by screen readers as a plain column header with a button inside it, so the current
sort column and direction are not conveyed. aria-sort is the attribute WAI-ARIA defines for exactly
this (ascending / descending / none on the sorted <th>, and it must be present in the
server-rendered payload, not written after hydration).
There is currently no supported way to add it from userland.
What exists today
Table.vue resolves two per-column <th> hooks off columnDef.meta:
<th
:data-pinned="header.column.getIsPinned()"
:scope="header.colSpan > 1 ? 'colgroup' : 'col'"
data-slot="th"
:class="ui.th({ class: [props.ui?.th, resolveValue(header.column.columnDef.meta?.class?.th, header)] })"
:style="[getColumnStyles(header.column), resolveValue(header.column.columnDef.meta?.style?.th, header)]"
>
So meta.class.th and meta.style.th are both supported and both receive the header context, which is
exactly what an aria-sort value needs (header.column.getCanSort(), header.column.getIsSorted()).
What is missing
There is no meta.attrs.th (or any other seam) that reaches the <th> element. Concretely:
- the
#<column>-headerslot renders inside the<th>, so it cannot set an attribute on it; props.ui.this class-only;:ui/app.config.ui.table.slots.thare class-only;- passing
attrsthroughcolumnDef.metais silently dropped.
The same gap applies to <td>, <tr>, and the footer <th>, but the header case is the one with a
concrete accessibility consequence.
Reproduction
npx nuxi init+@nuxt/ui@4.10.0- Render a
UTablewithsortingenabled on at least one column. - Inspect the emitted
<thead>: the sortable<th>carriesdata-pinned,scope,data-slot,class
andstyle, but noaria-sort, in any sort state. - Try to add it: there is no
meta.attrs.th, and the header slot cannot reach the<th>.
Proposed fix
Either of:
A. Emit aria-sort natively (preferred — it is unambiguously correct for a sortable column):
:aria-sort="header.column.getCanSort()
? (header.column.getIsSorted() === 'asc'
? 'ascending'
: header.column.getIsSorted() === 'desc' ? 'descending' : 'none')
: undefined"
B. Add a generic attribute hook mirroring the two that already exist, resolved the same way:
v-bind="resolveValue(header.column.columnDef.meta?.attrs?.th, header)"
(B) also unblocks aria-describedby, abbr, and per-column test hooks without another API each time.
Workaround in use
A build-time vite transform that rewrites the vendored Table.vue and appends the :aria-sort binding
after the :scope anchor. It is pinned to @nuxt/ui@4.10.0 exactly, because it depends on library
internals and a minor bump would move the anchor. It throws at build time when the anchor disappears.
We would rather delete it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in Table.vue, where the existing meta.class.th and meta.style.th hooks are resolved on each header cell. Compare the native aria-sort and generic meta.attrs.th proposals, then verify the chosen approach on the sortable-column reproduction and confirm the attribute is present in the server-rendered thead for each sort state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nuxtjs, typescript
- Domain
- accessibility, frontend
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100