core accessibility principles for modern frameworks

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 Content
  • 2.4.4 Link Purpose (In Context)
  • 4.1.2 Name, Role, Value
Who owns the alt text at each layer of a component Three stacked layers with the decision each one can make. The content layer, where an editor or product owner supplies the image, is the only place that knows what the image means, so it owns the description. The feature layer, where a card or article component places the image, knows the context and owns whether the image is decorative here. The primitive layer, the shared image component, owns nothing about meaning and only enforces that a decision was made. An arrow shows the description travelling down the stack unchanged. Content layer — editor, product owner, designer Knows what the image means. Owns the description. Nobody below can recover it. Feature layer — card, article, gallery Knows the context. Owns whether the image is decorative in this placement. Primitive layer — the shared Image component Knows nothing about meaning. Owns only the rule that a decision must exist. description travels down unchanged

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 alt to 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 decorative when 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.
The same image, described three ways Three descriptions of one screenshot of a failing test run, rated. Describing the appearance, a screenshot of a terminal, is rejected because it names the medium rather than the content. Describing everything visible, listing every line of output, is rejected as too long. Describing the point, the axe test failing with two colour contrast violations, is accepted because it carries the information the reader would otherwise lose. "Screenshot of a terminal window" Names the medium, not the content. The reader learns nothing they could act on. "Terminal showing npm test, 42 passing, 1 failing, colour contrast…" Transcribes everything visible. Too long to hear, and the point is buried at the end. "The axe test failing with two colour-contrast violations" Carries exactly what the screenshot was included to show. One sentence, no filler.

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.
Where a content pipeline can require a description A four-stage pipeline. At upload, the form requires either a description or a decorative checkbox. In storage, the record keeps both fields so a missing description is distinguishable from a decorative image. At publish, validation blocks content whose images have neither. At render, the component uses the stored decision and reports anything missing to a dashboard rather than guessing. A note underneath states that a nullable single field cannot tell decorative apart from forgotten. Upload description field or decorative box, beside the preview Storage two fields: altText and decorative Publish validation blocks images with neither Render uses the decision, reports gaps A single nullable altText column cannot distinguish "decorative" from "nobody filled this in" — so store both. Validating at publish keeps drafts fast while keeping published pages complete.

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-alt and object-alt rules catch absences and some duplication — see Automated Accessibility Testing with axe-core.
  • Type check. Delete the alt prop from a call site and confirm the build fails. If it compiles, the contract is not being enforced.

Common Accessibility Mistakes

  • Defaulting alt to 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 renders alt="" 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 ![alt](src) 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.