Require an accessible name on Pluma components rendered without visible text.
Use this rule to catch icon-only controls that ship with no accessible name.
A Button with isIconOnly discards its children, so any visible text you
pass is dropped from the output. Without aria-label the button reaches a screen
reader as "button" with nothing else — the icon conveys nothing.
Use the native aria-label attribute. Button has no ariaLabel prop in
either framework: React spreads the unknown prop without emitting an attribute,
and Ember drops the unrecognised argument, so the camelCase form leaves the button
nameless. Some other Pluma components (TextField, ProgressBar, Select) do declare
ariaLabel — Button is not one of them.
Types cannot enforce this. In Ember the name is usually a splattribute, which component code and Glint can never see, so a lint rule is the only check that works across both frameworks.
The rule reports an imported Pluma Button that renders no visible text and has
no aria-label or aria-labelledby. Only those native attributes are accepted,
because only they produce a real accessible name on a Button — the camelCase
ariaLabel / ariaLabelledby forms are not props of Button and are discarded, so
accepting them would let the rule pass on a nameless button.
"Renders no visible text" is decided from the children rather than from
isIconOnly alone, because Button discards its children only when isIconOnly
is set and an icon is supplied:
isIconOnly | icon prop | children | Renders | Reported |
|---|---|---|---|---|
| yes | yes | text | icon only — children dropped | yes, as discardedLabel |
| yes | no | text | the text | no — it is named |
| no | yes | none | icon only | yes |
| no | yes | text | icon + text | no — it is named |
| yes | any | icon element only | icon only | yes |
A dynamic flag such as isIconOnly={isCompact} counts as set: the button renders
icon-only in at least one state, and needs a name in that state.
These are not reported:
...attributes, because the caller may supply
the name and the rule cannot see through a spread.{label} — which may be text, so the
rule stays quiet rather than erroring on correct code.isIconOnly. A bare <Button /> is a different
problem and not this rule's concern.aria-hidden, which are removed from the accessibility tree and
so need no name — Pluma's own FileField does this for a decorative trigger whose
real name lives on a sibling input.title is deliberately not accepted. It only appears on hover, is unavailable on
touch, and is the last resort in accessible name computation, so an icon-only
control needs a name that is always present.
aria-label and aria-labelledby name a control, and aria-hidden exempts it, only
as native attributes. An @-prefixed form does not, so it never satisfies this
rule:
Arguments (@foo) and attributes (foo) are separate channels in Glimmer, and
...attributes forwards only the attribute one. A hash of props cannot be splatted
as attributes, so a prop reaches the DOM only if some component writes foo={{…}} in
a template — and no Pluma component writes an ARIA attribute out of an argument. An
@aria* argument is read into restProps, handed to Box as unsafe_props, used
solely to compute sprinkle class names, and then dropped.
Types do not catch this either: Box is polymorphic and intersects native HTML
attribute types into its Args, which makes aria-label a valid key on a component
that never reads it. Glint rejects @ariaLabel, but not @aria-label.
The fix is always to remove the @.
This rule only ensures such a form cannot pass as a name. It does not report the inert argument itself — that applies to most Pluma components rather than just icon-only ones, and fires on controls that are correctly named, so it belongs in a rule of its own.