Writing Alt Text for Component-Driven UIs
In a plain HTML page, the person writing the <img> tag is the person who knows what the image means. In a component codebase they are almost never the same person: an engineer writes <Avatar user={user} />, a designer picked the asset, and an editor uploaded it through a CMS. The alt attribute ends up written by whoever is closest to the markup, which is usually whoever knows least about the meaning. This guide is about moving that decision to where the knowledge is, and encoding it so it cannot be skipped.
It applies the rules from Accessible Images, Icons & Media in Component UIs to real component APIs.
WCAG Success Criteria Addressed:
1.1.1 Non-text Content2.4.4 Link Purpose (In Context)4.1.2 Name, Role, Value
Prerequisites
- A component library with a shared image or media primitive (or the freedom to add one).
- TypeScript, if you want the compiler to enforce the contract; the patterns work without it, with weaker guarantees.
- Access to wherever images enter the system — a CMS, an upload form, a content collection.
Push the Decision Up, Enforce It Down
The primitive cannot know whether a photograph is meaningful. What it can do is refuse to render until someone says. That single rule changes alt text from an easily-forgotten attribute into a required input.
Implementation Guidelines:
- Make the shared image component accept either a description or an explicit decorative flag, with no third option.
- Never default
altto the file name, the slug, the caption or an empty string. A silent default is a decision made by the wrong layer. - Let feature components override a description with
decorativewhen the context makes the image redundant — a thumbnail next to its own headline, for example. - Keep the prop names blunt (
alt,decorative). Clever names get misused.
type Described = { alt: string; decorative?: never };
type Decorative = { decorative: true; alt?: never };
export type ImageProps = (Described | Decorative) &
Omit<React.ComponentProps<'img'>, 'alt'>;
// A11y rationale: 1.1.1 Non-text Content — the union makes "no decision" a
// compile error rather than a silently empty attribute.
export function Image({ alt, decorative, ...rest }: ImageProps) {
return <img alt={decorative ? '' : alt} {...rest} />;
}
Writing the Sentence
Once the decision is made, the text itself is a small writing problem with a few reliable rules. The best test is functional: read the page aloud, substituting your alt text for the image, and see whether the paragraph still works.
Implementation Guidelines:
- Describe the meaning, not the appearance. "Support tickets fell 40% after the July release" beats "line chart with a downward slope".
- Start with the substance. Screen reader users hear the first few words in a list of images; "Chart showing…" wastes them.
- Skip "image of", "photo of" and "graphic of" — the role is announced already.
- Keep it to one or two sentences. Anything longer belongs in the surrounding prose or a caption.
- Match the register of the page. Alt text is content, and it is read by the same people who read everything else.
Avatars, Thumbnails and Logos
Three recurring cases account for most of the arguments in code review, and each has a defensible default.
Implementation Guidelines:
- Avatars next to a name: decorative. The name is already there; describing the photo adds "photo of Kim Nguyen" before every "Kim Nguyen".
- Avatars alone (a stack of contributors, a comment with no visible name): describe as the person —
alt="Kim Nguyen"— because the image is the identification. - Thumbnails inside a link that also contains the title: decorative. One link, one name; see the card pattern in the parent guide.
- Logos linking home: the alt text is the destination —
alt="Acme home"— not the brand's tagline or the file name.
// The same avatar, two contexts, two correct answers.
<span className="byline">
<Avatar user={user} decorative />
{user.name}
</span>
<ul className="contributors">
{contributors.map((user) => (
<li key={user.id}><Avatar user={user} alt={user.name} /></li>
))}
</ul>
Alt Text in a Content Pipeline
Editor-supplied images are where the contract most often breaks, because the CMS field is optional and nobody sees the consequence. Fixing it is a schema change more than a code change.
Implementation Guidelines:
- Make the description a required field on the media model, with a "this image is decorative" checkbox as the explicit alternative.
- Show the field next to the image preview at upload time, not in a separate accessibility tab where it is never opened.
- Validate on publish, not on save, so drafts stay frictionless while published content stays complete.
- Report missing descriptions on existing content as a work queue with links straight to the edit screen — a list of URLs nobody can act on gets ignored.
How to Verify
- Read-aloud test. Read each paragraph with the alt text spoken in place of the image. If the sentence becomes redundant, the image should be decorative; if it becomes incomplete, the description is too thin.
- Images-off pass. Disable images in the browser and scan the page. Every remaining string should read like content, not like a file name.
- Screen reader list. Open the images or graphics rotor. Duplicated names, file extensions and "image" prefixes all show up immediately in that list.
- Automated scan. axe's
image-alt,image-redundant-altandobject-altrules catch absences and some duplication — see Automated Accessibility Testing with axe-core. - Type check. Delete the
altprop from a call site and confirm the build fails. If it compiles, the contract is not being enforced.
Common Accessibility Mistakes
- Defaulting
altto the filename. Produces "IMG_4821 dot jpeg" and looks, to an automated scan, like a described image. - Describing the avatar next to a visible name. The user hears the name twice, once with "photo of" in front of it.
- Repeating the caption in the alt text. The caption is already announced; the duplication doubles the length of every figure.
- Marking an informative chart decorative to silence a lint rule. The rule is satisfied and the information is gone. Add a real alternative instead.
- Writing alt text for search engines. Keyword-stuffed descriptions are unpleasant to listen to and describe nothing.
- Leaving the decision to the primitive. A shared
<Image>that quietly rendersalt=""when the prop is missing turns every forgotten description into an invisible bug.
Conclusion
Alt text goes wrong in component codebases for a structural reason, not a knowledge one: the decision is made in the file furthest from the meaning. Fix the structure and the writing gets easy. Require an explicit choice at the primitive, let feature components mark images decorative in context, capture descriptions where content enters the system, and validate at publish. Then the only remaining work is the sentence itself — describe the point, not the picture, and check it by reading the page aloud.
Frequently Asked Questions
What if nobody knows what the image was meant to convey? That is a content problem worth surfacing rather than papering over. Mark it decorative only if the page still makes sense without it; otherwise flag it for the person who owns the content. Inventing a description that sounds plausible is the worst outcome, because it removes the evidence that anything is missing.
Should alt text end with a full stop? It makes no functional difference to most screen readers, though a full stop encourages a natural pause. Consistency matters more than the choice; pick one and put it in the content style guide.
Do decorative images still need the alt attribute?
Yes — alt="", explicitly. An entirely missing alt causes some screen readers to announce the file name or the URL, which is precisely what the empty string prevents.
How do I handle images inside markdown content?
Markdown's  syntax has a slot for it, so the requirement is editorial rather than technical: review that authors are filling it in, and lint published content for empty alt text on images that are clearly informative. Where the pipeline renders markdown images through your own component, apply the same union-type contract.
How long should the review take in a pull request? Seconds, if the contract is in place. The reviewer's only job is to read the description and ask whether it says what the image is for — the compiler has already confirmed that a decision exists. Where reviews get long is in codebases with no contract, because every image becomes an individual negotiation about whether it needed a description at all.
What about images whose meaning depends on state? Describe the current state, not the general case. A status badge rendering a green tick should read "Passed", not "Status indicator" — the point of the image is which state it is in. Build the description from the same data that chooses the icon, so the two can never disagree.
Is alt text needed on images that are also described in the caption?
Not usually. If the caption carries the meaning, alt="" avoids hearing the same sentence twice — the figure's name comes from the caption. Reserve a distinct description for cases where the image shows something the caption does not mention.
Related guides
- Accessible Images, Icons & Media in Component UIs — the parent guide and the three-bucket decision.
- Making Inline SVG Icons Accessible in React — naming rules for glyphs and icon buttons.
- Catching ARIA Mistakes with TypeScript and ESLint — making the contract a compile error.
- Semantic HTML vs ARIA in Component Trees — where accessible names come from in general.