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

    a11ybugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions