Which components of popular libraries would be a CSS class on a built-in element if CSS could set ARIA (and focusability)? Each component links to its source; rows link to the matching use case where one exists.
Web Awesome (branch next, October 2026) has 73 components.
It also ships native styles for button, details, dialog, hr, checkboxes, radios, text fields, select, input[type=range], input[type=color] and progress, so for many components the styling half of the class already exists in the same package; the component exists for the other half.
| Count | |
|---|---|
| A class on a native element today, no ARIA needed | 10 |
| A class once CSS can set ARIA | 32 |
A class once CSS can set ARIA and focusgroup |
7 |
| Stays a component | 24 |
Columns: the native element the class would go on, what CSS already covers, what ARIA in CSS adds (the blocker), and what JS would remain, if any.
Nothing stands in the way; the component wraps presentation.
| Component | Element | Notes |
|---|---|---|
| badge | span |
|
| callout | div |
|
| card | article/div |
header, media and footer are slots → children |
| tag | span + button |
the remove button is markup; the wa-remove event is the author's listener either way |
| button | button / a |
native styles already cover both; aria-busy from [loading] is the one ARIA write (Loading buttons) |
| button-group | div |
the JS toggles hover/focus classes on neighbours for border collapsing, which is :has() today |
| popup | any | anchor positioning covers placement, flip, shift, arrows, anchor-size(); virtual elements stay JS |
| animated-image | img |
image-animation: controlled is the CSS version, with UA-assigned semantics |
| scroller | div |
overflow + scroll-driven animations for the shadows; focused scroll containers already handle Home/End. The role="region" is the scrollable regions case |
| textarea | textarea |
native styles exist; resize is CSS; autosize is field-sizing: content; the character count is a small output + JS |
| Component | Element | CSS already covers | ARIA in CSS adds | Residue |
|---|---|---|---|---|
| skeleton | div |
everything | aria-busy: true (Skeleton) |
|
| spinner | div |
everything | role: progressbar; aria-label: "Loading" (Spinner) |
|
| avatar | span/img |
shape, initials, icon | role: img; aria-label: attr(label) (Avatar) |
|
| divider | hr |
native styles, orientation, label via ::before |
aria-orientation from [orientation] (orientation); a labelled divider needs aria-label |
|
| icon | span |
mask-image |
aria-hidden unless [label], then role: img; aria-label: attr(label) (decorative elements) |
fetching SVG by name stays JS; a mask-based icon set does not |
| checkbox | input[type=checkbox] |
native styles | aria-describedby: --hint via ref-scope (relationships); :user-invalid { aria-invalid } (form validation) |
setCustomValidity() is native |
| switch | input[type=checkbox switch] |
native styles | role: switch where the UA does not (polyfills); aria-describedby |
|
| radio | input[type=radio] |
native styles | aria-describedby |
|
| radio-group | fieldset |
native styles | aria-orientation from [orientation]; aria-describedby |
|
| checkbox-group | fieldset |
native styles | same | |
| input | input |
native styles, decorations via ::before/::after or siblings |
aria-describedby: --hint; :user-invalid; the password toggle's aria-pressed |
clearing and type flipping are one line of JS each |
| number-input | input[type=number] |
native styles | aria-describedby; :user-invalid |
custom stepper icons |
| select (single) | select |
customizable select, ::picker() |
aria-describedby; :user-invalid; ::picker() roles where the picker is a grid |
multiple-with-tags and lazy loading stay JS |
| option | option |
customizable select | nothing, unless relabelled (select listboxes) | |
| details | details |
native styles, ::details-content transitions, icon placement |
::details-content { role: region; aria-labelledby: --summary } (accordion panels) |
|
| accordion, accordion-item | details[name] |
exclusive accordion, heading wrapper is markup | same as details | expand/collapse all is the author's loop either way |
| dialog | dialog |
native styles, closedby for light dismiss, commandfor to open |
aria-labelledby: --title via ref-scope (dialog labels); dialog:modal { aria-modal: true } is UA |
autofocus covers initial focus |
| drawer | dialog |
dialog + placement, transitions | same as dialog | |
| popover | [popover] |
popover + anchor positioning + commandfor; the UA sets aria-expanded on the invoker |
nothing on the popover itself; roles when it holds a widget | |
| tooltip | ::tooltip / [popover] |
interest invokers + anchor positioning | role: tooltip (UA for ::tooltip); the shared description list |
manual show/hide is showPopover() |
| progress-bar | progress or div |
native styles; width: var(--value) |
aria-valuenow: var(--value) (values that draw the UI) |
|
| progress-ring | progress or div |
conic-gradient from attr(value) / var(--value) |
same | |
| breadcrumb, breadcrumb-item | nav > ol > li > a |
separators via ::before, dropdown items are markup |
li:last-child > a { aria-current: page } (breadcrumbs) |
|
| step, stepper | nav > ol > li |
variants, orientation, icons | .active { aria-current: step } on host or button; .loading { aria-busy: true } (stepper) |
step-change announcements (one-shot, ariaNotify()) |
| pagination | nav > ol > li > a |
compact, summary via sibling-index()/sibling-count() |
aria-current: page; aria-disabled on boundary controls (pagination) |
generating the list from total/page-size is templating or the server |
| rating | fieldset > input[type=radio] |
the classic CSS-only widget: :checked, :hover and :has(~ :hover) for preview, extra radios for precision |
aria-readonly for read-only; radios are already a correctly announced value (rating) |
wa-hover event |
| slider (single) | input[type=range] |
native styles, datalist markers |
aria-valuetext: control-value() "%" (slider) |
the value tooltip needs control-value() in content; range (two thumbs) stays JS |
| carousel, carousel-item | div with scroll snap |
::scroll-marker, ::scroll-button(), scroll-snap, slides per page via scroll-snap-type and sizing (CSS carousels) |
aria-label: sibling-index() " of " sibling-count() (carousel slides); :snapped { aria-current }; aria-hidden/interactivity: inert on off-screen slides (scroll-snap) |
loop, autoplay and mouse dragging stay JS |
These need roving focus or arrow-key traversal as well, which is the other half of the program.
| Component | Element | CSS already covers | ARIA + focusgroup adds |
Residue |
|---|---|---|---|---|
| tab, tab-group, tab-panel | details[name] or radio group |
layout, overflow scrolling (::scroll-button()) |
focusgroup: tablist; aria-selected from [open]/:checked; role: tabpanel; interactivity: focusable (tabs) |
closable tabs are a button plus the author's handler; manual activation is what details does natively |
| dropdown, dropdown-item | details + ::details-content |
anchor positioning, nested details for submenus, :checked for checkable items |
focusgroup: menu on the panel (dropdown menus); menuitemcheckbox and aria-checked on checkbox inputs, aria-haspopup on submenu summaries (menu items) |
typeahead, submenu hover timing |
| tree, tree-item | nested details |
indent guides, icons, [open] |
role: tree/treeitem; aria-level from nesting; aria-expanded from [open], none on leaves; aria-selected; focusgroup (tree view) |
multi-selection, indeterminate parents, lazy loading |
Behavior that is neither presentation nor semantics.
| Component | Why |
|---|---|
| tag-input, otp-input, time-input, known-date | composite editing: tokenisation, segment focus, parsing, locale rules |
| color-picker | the panel: 2D picking, format conversion, eyedropper. A plain swatch is input[type=color], which native styles already cover |
| split-panel | CSS resize has no keyboard path and resizes the element rather than a divider |
| comparison | the range value drives a sibling's clip-path; one line of JS, no CSS path |
| copy-button | clipboard |
| qr-code | canvas |
| format-bytes, format-date, format-number, relative-time | Intl, timers |
| include, markdown, random-content | fetching and parsing |
| animation, intersection-observer, mutation-observer, resize-observer | declarative wrappers over JS APIs |
| toast, toast-item | one-shot announcements and timers |
| zoomable-frame | zoom on an iframe is CSS; the controls and theme sync are JS |
| page | a layout; not ARIA-blocked, and mostly CSS already |
- The blocker is semantics, not styling. For 39 components the styling is already CSS, and for the form controls,
details,dialog,progressandhramong them, Web Awesome's own native stylesheet already styles the element the class would go on. What the component adds isrole,aria-*and (for seven) focus management. - Form controls are the biggest group. Ten wrappers around native inputs exist mainly to attach a label and a hint, to sync
aria-invalid, and to style the result;ref-scopeand:user-invalidcover the first two and native styles the third. - Native behaviour is usually already there.
details,dialog,popover, scroll snap, customizable select and radio groups supply the interaction; the components re-implement it in JS so they can own the attributes. Rating is the clearest case: the radio version is more accessible than the slider the component exposes. - The residue is small and one-shot. Where JS remains in the "class" tiers it is clipboard, timers, announcements, or a single line that flips an attribute, none of it the state-sync code that ARIA in CSS removes.