# Label HTML Illustrations

**MUST** · **ID:** `content-illustration-label` · **Category:** content
**Source:** [Rauno](https://interfaces.rauno.me/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/content-illustration-label

> MUST: Give a div-built illustration or chart `role="img"` + `aria-label` on the wrapper and `aria-hidden="true"` on its inner nodes, so screen readers announce it once instead of walking every group.

Give an illustration built from HTML an explicit aria-label so it is announced as one image, not as a DOM tree

A chart or illustration assembled from divs has no single accessible node, so assistive tech walks it element by element and reads out a stream of meaningless groups. Put role="img" plus an aria-label that states what the picture conveys on the wrapper, and mark the inner nodes aria-hidden, so the whole thing collapses into one announcement.

## Rule snippet

```tsx
<div role="img" aria-label="Revenue grew 40% in Q3">
  <div className="bar" aria-hidden="true" />
</div>
```

## Bad — do not do this

`content-illustration-label-bad`

```tsx
const BARS = [40, 55, 35, 70, 90];

export function IllustrationLabelBad() {
  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="bg-card border border-border rounded-lg p-4 space-y-3">
        <div className="flex items-end gap-2 h-24">
          {BARS.map((h, i) => (
            <div
              key={i}
              className="flex-1 rounded-t bg-primary/60"
              style={{ height: `${h}%` }}
            />
          ))}
        </div>
        <div className="p-2 bg-muted rounded text-xs text-error space-y-0.5 font-mono">
          <p className="text-foreground font-sans font-medium">Screen reader</p>
          <p>group … group … group … group … group … group</p>
        </div>
      </div>
      <p className="text-xs text-error">
        The illustration is a stack of divs, so it is announced element by
        element — six meaningless nodes and not one number
      </p>
    </div>
  );
}
```

## Good — do this

`content-illustration-label-good`

```tsx
const BARS = [40, 55, 35, 70, 90];

export function IllustrationLabelGood() {
  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="bg-card border border-border rounded-lg p-4 space-y-3">
        <div
          role="img"
          aria-label="Bar chart: weekly deploys rose from 12 to 27 over 5 weeks"
          className="flex items-end gap-2 h-24"
        >
          {BARS.map((h, i) => (
            <div
              key={i}
              aria-hidden="true"
              className="flex-1 rounded-t bg-primary/60"
              style={{ height: `${h}%` }}
            />
          ))}
        </div>
        <div className="p-2 bg-muted rounded text-xs text-success space-y-0.5 font-mono">
          <p className="text-foreground font-sans font-medium">Screen reader</p>
          <p>"Bar chart: weekly deploys rose from 12 to 27 over 5 weeks, image"</p>
        </div>
      </div>
      <p className="text-xs text-success">
        role="img" plus an aria-label collapses the DOM tree into one node that
        carries the meaning the picture conveys
      </p>
    </div>
  );
}
```

## References

- [interfaces.rauno.me](https://interfaces.rauno.me/)
- [MDN: img role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/img_role)
- [ARIA APG: Names and Descriptions](https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/)
