Skip to content

IconButton hard-codes aria-label to the icon name and ignores a consumer-provided aria-label #1056

Description

@natalyjazzviolin

Summary

IconButton sets aria-label to the raw icon token name (e.g. sparkle, trash, cross) and there is no way to override it. Because aria-label={iconName} is placed after the {...props} spread, a consumer-supplied aria-label is silently clobbered. This means every IconButton exposes a non-human-meaningful accessible name to screen-reader users, with no escape hatch.

Current behavior

src/components/IconButton/IconButton.tsx (on main):

export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
  ({ type = 'primary', icon, size, disabled, className, ...props }, ref) => {
    const iconName = icon ? icon.toString() : 'unknown icon';

    return (
      <button
        {...props}
        className={cn(iconButtonVariants({ type, size }), className)}
        disabled={disabled}
        ref={ref}
        role="button"
        aria-label={iconName}   // <-- after {...props}, so it overrides any consumer aria-label
      >
        <Icon name={icon} size="sm" />
      </button>
    );
  }
);

Two distinct problems:

  1. Poor default accessible name. With no label provided, the accessible name falls back to the icon token ("sparkle", "trash", …), which is meaningless to assistive-tech users describing the action. Unlike Button, IconButton has no label/ariaLabel prop.
  2. The default can't be overridden. IconButtonProps extends HTMLAttributes<HTMLButtonElement>, so the type advertises aria-label as a valid prop, but the implementation ignores it at runtime (it's spread first, then overwritten). The only override that currently survives is aria-labelledby (spread via ...props, and per the accessible-name spec it outranks aria-label) — which requires a referenced DOM node and is non-obvious.

This is a type/runtime contract mismatch, not just a styling default — which is why I'd classify it as a bug rather than an enhancement.

Expected behavior

A consumer-provided accessible name should win, and there should be an explicit, discoverable way to set one.

Suggested fix (small, backward-compatible)

Respect a consumer-provided aria-label, falling back to the icon name only when none is given:

<button
  {...props}
  className={cn(iconButtonVariants({ type, size }), className)}
  disabled={disabled}
  ref={ref}
  role="button"
  aria-label={props['aria-label'] ?? iconName}
>

and/or add an explicit label prop mirroring Button:

export interface IconButtonProps extends HTMLAttributes<HTMLButtonElement> {
  // ...
  /** Accessible name for the button. Falls back to the icon name if omitted. */
  label?: string;
}

Impact

Affects every IconButton instance. In the control-plane apps/web app alone there are ~124 instances, each currently announcing an icon token as its accessible name.

Environment

  • Observed on @clickhouse/click-ui v0.2.1-rc.8 (styled-components implementation) and confirmed still present on main (CSS-modules implementation).

Metadata

Metadata

Assignees

No one assigned

    Labels

    a11yAccessibility improvementsbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions