Accessibility pattern · Disclosure and content

Code block

The code is a labelled region that takes focus while it scrolls, so it can be read and moved with the keyboard. Line numbers come from CSS counters with empty alternative text, so screen readers skip them and Copy leaves them out.

WCAG criteria
6
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

JavaScript format-price.js

                            // Shows a price in paise as rupees: 249900 → "₹2,499.00"
                            export function formatPrice(paise, locale = "en-IN") {
                              const rupees = paise / 100;
                              return new Intl.NumberFormat(locale, {
                                style: "currency",
                                currency: "INR",
                              }).format(rupees);
                            }
                            
                            const order = { id: "AA-20418", totalPaise: 249900, method: "UPI" };
                            console.log(`Order ${order.id}: ${formatPrice(order.totalPaise)} paid by ${order.method}`);
                            

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to Wrap lines, then Copy, then into the code while it is wider than its box.
Enter or SpaceOn Wrap lines, wraps long lines or puts them back; on Copy, copies the code without its line numbers.
Arrow Left or Arrow RightWith the code focused, scrolls it sideways.
Arrow Up or Arrow DownWith the code focused, scrolls it up and down when it is taller than its box.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the toggleWrap lines, toggle button, not pressed
Enter turns wrapping onPressed
Focus reaches CopyCopy, button, format-price.js
The code is copiedCopied
Tab reaches the codeformat-price.js JavaScript, region

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-code-block" data-ap-code-block>
  <div class="ap-code-block__bar">
    <div class="ap-code-block__meta">
      <span class="ap-code-block__lang" id="code-block-lang">JavaScript</span>
      <span class="ap-code-block__file" id="code-block-file" translate="no">format-price.js</span>
    </div>
    <div class="ap-code-block__actions">
      <button type="button" class="ap-code-block__btn" aria-pressed="false" data-ap-code-wrap>
        <svg class="ap-code-block__icon ap-code-block__icon--off" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16"/><path d="M4 12h13a3 3 0 0 1 0 6h-4"/><path d="m15 16-2 2 2 2"/><path d="M4 18h5"/></svg>
        <svg class="ap-code-block__icon ap-code-block__icon--on" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
        <span>Wrap lines</span>
      </button>
      <button type="button" class="ap-code-block__btn" aria-describedby="code-block-file" data-ap-code-copy>
        <svg class="ap-code-block__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
        <span>Copy</span>
      </button>
      <span class="ap-code-block__status" role="status">
        <svg class="ap-code-block__icon ap-code-block__icon--ok" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
        <svg class="ap-code-block__icon ap-code-block__icon--err" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg>
        <span class="ap-code-block__msg"></span>
      </span>
    </div>
  </div>
  <pre class="ap-code-block__pre" tabindex="0" role="region" aria-labelledby="code-block-file code-block-lang" translate="no"><code class="ap-code-block__code">
<span class="ap-code-block__line"><span class="ap-code-block__cm">// Shows a price in paise as rupees: 249900 → "₹2,499.00"</span></span>
<span class="ap-code-block__line"><span class="ap-code-block__kw">export function</span> <span class="ap-code-block__fn">formatPrice</span>(paise, locale = <span class="ap-code-block__str">"en-IN"</span>) {</span>
<span class="ap-code-block__line">  <span class="ap-code-block__kw">const</span> rupees = paise / <span class="ap-code-block__num">100</span>;</span>
<span class="ap-code-block__line">  <span class="ap-code-block__kw">return new</span> Intl.<span class="ap-code-block__fn">NumberFormat</span>(locale, {</span>
<span class="ap-code-block__line">    style: <span class="ap-code-block__str">"currency"</span>,</span>
<span class="ap-code-block__line">    currency: <span class="ap-code-block__str">"INR"</span>,</span>
<span class="ap-code-block__line">  }).<span class="ap-code-block__fn">format</span>(rupees);</span>
<span class="ap-code-block__line">}</span>
<span class="ap-code-block__line"></span>
<span class="ap-code-block__line"><span class="ap-code-block__kw">const</span> order = { id: <span class="ap-code-block__str">"AA-20418"</span>, totalPaise: <span class="ap-code-block__num">249900</span>, method: <span class="ap-code-block__str">"UPI"</span> };</span>
<span class="ap-code-block__line">console.<span class="ap-code-block__fn">log</span>(<span class="ap-code-block__str">`Order </span><span class="ap-code-block__kw">${</span>order.id<span class="ap-code-block__kw">}</span><span class="ap-code-block__str">: </span><span class="ap-code-block__kw">${</span><span class="ap-code-block__fn">formatPrice</span>(order.totalPaise)<span class="ap-code-block__kw">}</span><span class="ap-code-block__str"> paid by </span><span class="ap-code-block__kw">${</span>order.method<span class="ap-code-block__kw">}</span><span class="ap-code-block__str">`</span>);</span>
</code></pre>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The code sits in a pre named by its file name and language, and the language is text in the bar, not a color or a logo.

  • 1.4.3 Contrast (Minimum) Level AA

    Every syntax color is a token that clears 4.5:1 on the code background in both themes; comments are italic as well as grey.

  • 1.4.10 Reflow Level AA

    Wrap lines lets long lines fold at narrow widths and high zoom, so nobody has to scroll sideways to read a line.

  • 2.1.1 Keyboard Level A

    While the code is wider than its box it takes focus, so the arrow keys can scroll it; both buttons work with Enter and Space.

  • 4.1.2 Name, Role, Value Level A

    Wrap lines is a toggle button with aria-pressed, and Copy is described by the file name it copies.

  • 4.1.3 Status Messages Level AA

    Copied, or the reason it could not copy, is announced from a polite status region without moving focus.

Usage

When to use it

Use it

  • Code readers are meant to copy: install commands, configuration, short examples in docs and articles.
  • Samples long enough to need line numbers when they are discussed in the text around them.

Use something else

  • A single command or value in a sentence: an inline code element is enough.
  • Code people edit in place: use a real editor component, which has its own keyboard model.
  • Output from a running program: a log region that announces new lines fits better.

Common failures

How it usually goes wrong

  • A scroll box the keyboard cannot reach

    A pre with overflow: auto scrolls only with a mouse or touch. tabindex=0 lets keyboard users focus and scroll it, and the region role with a name says what it is.

  • Line numbers in the text

    Numbers typed into the markup are read before every line and pasted with the code. CSS counters with empty alternative text are neither.

  • A Copy button that says nothing

    If the only sign of success is an icon that flickers, screen reader users never learn whether it worked. A status region says Copied.

  • A label that changes to Copied

    Renaming the button for a moment confuses anyone who lands on it then. The button stays Copy; the confirmation is a separate message.

  • Syntax colors that fail contrast

    Many editor themes put comments and strings near 3:1. The colors here are the token set the contrast test measures.

  • A wrap switch with no state

    A button whose look changes but whose state is not exposed leaves people guessing. aria-pressed says whether wrapping is on.

Notes

Building it

  • content: counter(…) / "" gives the number empty alternative text. Browsers that do not support it fall back to the first declaration and read the number, which is a nuisance, not a barrier.
  • Copy joins the text of each line with a line break, so the indentation in your HTML never reaches the clipboard.
  • If the Clipboard API is blocked, the script selects the code and says so, so people can copy it with their own keys.
  • The script removes the tab stop from the code when it fits, so keyboard users are not sent to a box that does not scroll.
  • translate="no" on the pre stops browser translation tools from rewriting the code.

Sources: CSS Generated Content: alternative text for content · WAI-ARIA Authoring Practices: Button (toggle) · MDN: Clipboard.writeText()

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction