Is your body text too small and too grey? An accessibility pass you can ship this week

2026-11-05来源 dev.to UXEnglish

A paragraph set at 13px in #999 on a white card looks calm in a mockup and is unreadable on a phone held outdoors. It also fails the contrast rule on its own: #999999 on white is 2.85:1.

None of what follows needs a redesign. It needs a floor — a minimum type size, a contrast rule, alt text written on purpose, and a focus ring nobody deletes.

Body text: pick a floor and keep it

Sixteen pixels is the absolute floor for body copy, and on a marketing page 18px reads better than 16. Secondary text — captions, metadata, form hints — should not go below 14px. Line height of 1.5 to 1.6 for anything longer than a sentence, and a measure of roughly 60 to 75 characters per line.

:root { --text: #1a1a1a; --text-muted: #5a5a5a; --step-0: 1.125rem; /* 18px body */ --step--1: 0.875rem; /* 14px, the floor for secondary text */ } body { font-size: var(--step-0); line-height: 1.6; color: var(--text); background: #ffffff; } .prose { max-width: 65ch; } .meta { font-size: var(--step--1); color: var(--text-muted); /* ~6.9:1 on white */ } 

Set the viewport without blocking zoom. user-scalable=no and maximum-scale=1 are the two values that stop people from enlarging text on a phone, and both still show up in starter templates.

<meta name="viewport" content="width=device-width, initial-scale=1"> 

Contrast: the numbers, and where they apply

Normal-size text needs 4.5:1 against its background. Large text — 24px and up, or 18.66px and up when bold — needs 3:1, and so do borders, icons and control outlines that carry meaning.

#767676 is the lightest grey that still clears 4.5:1 on white, at 4.54:1. One step lighter, #999999, is 2.85:1 and fails. That single pair of hex values explains most of the grey text on the web.

Things that get forgotten in the audit:

  • Placeholder text inside inputs is text. It needs to pass, and it also should not be the only label.
  • Text laid over a photo needs a solid or near-solid overlay behind it. A gradient that works on the desktop crop will not work on the mobile crop.
  • Links inside body copy need more than a color difference. Underline them, or the link disappears for anyone who cannot separate the two hues.
  • Required fields marked only in red are invisible to a chunk of your readers. Add the word.
  • Disabled buttons are exempt from the contrast rule, which makes "disable until valid" a bad pattern for a primary action. Let the button work, validate on submit, and show a readable message.

Check every color pair you actually ship, not the ones in the palette file — hover states, footer links on the dark band, and the text inside a filled button included.

Alt text is about purpose, not description

The question is not "what is in this picture" but "what is this picture doing here".

  • Decorative shapes, background flourishes, spacer images: alt="". An empty alt is not laziness; it is the instruction to skip the image.
  • A logo at the top of the page: the company name. Nothing else.
  • A product photo in a grid: the product and the variant a person would use to tell it apart — material, size, color.
  • An icon that is the only content of a link: the destination. Better still, put visible text next to it and drop the image from the accessible name.
  • A chart: a short alt naming what it plots, plus a real sentence or table of the numbers nearby. No alt text can carry a chart.
  • Captions and alt text should not repeat each other. If the caption says it, the alt does not need to.

Skip "image of" and "photo of" — the screen reader already says it is an image.

Keyboard: unplug the mouse and use the page

Tab from the address bar to the last link in the footer. Every stop should show a visible ring, in an order that matches the reading order, with nothing skipped and nothing trapped.

:focus-visible { outline: 3px solid #0b5fff; /* ~5.1:1 on white, clears the 3:1 non-text rule */ outline-offset: 2px; } 

If a stylesheet in the project contains outline: none or outline: 0 on :focus, that is the bug. Replace it with the rule above rather than deleting the line and hoping.

A skip link is four lines and saves a keyboard user from tabbing through the whole nav on every page.

<a class="skip" href="#main">Skip to content</a> <!-- ... --> <main id="main"> 
.skip { position: absolute; left: 0; top: 0; transform: translateY(-120%); background: #ffffff; color: #1a1a1a; padding: 0.75rem 1rem; z-index: 100; } .skip:focus-visible { transform: translateY(0); } 

Use <button> for anything that changes something on the page and <a href> for anything that goes somewhere. A clickable <div> is unreachable and unannounced; if you must keep it, add role="button", tabindex="0" and a keydown handler for Enter and Space, then ask why the <button> was not an option.

For dialogs, a native <dialog> opened with showModal() keeps focus inside and closes on Escape, which removes most of the hand-written focus-trap code. For menus and pickers, native <select> and <details> behave correctly with a keyboard by default. Hover-only navigation is unusable on a keyboard and on a touch screen at the same time.

Form errors have to be spoken

An error message that only changes color is an error message half your readers never receive. Tie the message to the field and let it be announced when it appears.

<label for="email">Email</label> <input id="email" name="email" type="email" autocomplete="email" aria-describedby="email-error" aria-invalid="true"> <p id="email-error" role="alert">Enter an email address, for example name@company.com.</p> 

Render the container empty on page load and fill it on submit. A message that is inserted into an existing live region is announced; one that arrives already populated inside freshly injected markup is less reliable.

Motion is the other thing people turn off. A short media query covers the carousels, the parallax and the smooth scrolling in one go.

@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } 

The pass to run before you publish

Work through this on the real staging site, not on a component library page.

  • Tab from the browser chrome to the end of the page with the mouse untouched. Visible ring at every stop, order matches the layout, no trap in a modal, no stop on a decorative element.
  • Zoom the browser to 200%. Nothing overlaps, nothing is clipped, no content disappears.
  • Narrow the window to 320px wide. No sideways page scroll — intentional scroll regions like a data table are fine.
  • Run a contrast checker on every text and background pair you ship, including placeholders, captions and hover states. 4.5:1 for body text, 3:1 for large text and for borders that carry meaning.
  • Turn on a screen reader for five minutes: NVDA on Windows, or VoiceOver on macOS with Cmd+F5. Listen to the navigation and the contact form. You are checking whether the labels make sense out loud, not whether every element is perfect.
  • Submit the contact form empty. The error is announced, the field is described by it, and focus lands somewhere sensible.
  • Open the accessibility panel in devtools and read the heading outline. One h1, no skipped levels, headings used for structure rather than for size.
  • Look at every image on the page: it either has alt text that says what it is for, or alt="".
  • Check tap targets on a phone. 24×24 CSS pixels is the floor with spacing, and primary actions want more room than that.
  • Finish with an automated scan and clear what it flags. It catches the mechanical failures — missing labels, low contrast, bad ARIA — and it cannot tell you whether your alt text is meaningful. That part is still yours.

原文:https://www.a8q9.cn/en/article/is-your-body-text-too-small-and-too-grey-an-accessibility-pass-you-can-ship-this-75aa94.html

相关文章