web3icons

Screen readers and long crypto addresses

How screen readers handle 0x and Base58 addresses, how to give truncated addresses a usable name, and how to label copy buttons and ENS names.

5 min read

An Ethereum address is 42 characters: 0x followed by 40 hexadecimal digits. A Solana address is a Base58 string, usually 32 to 44 characters long. Sighted people rarely read either. They glance at the first and last few characters and move on, which is why interfaces shorten addresses to something like 0x71C7…976F.

A screen reader can't glance. It has to turn the string into speech, and both the full and the shortened forms tend to come out as noise. The good news is that a few markup decisions make addresses much easier to identify, copy and check.

What a screen reader actually does with an address

There is no single answer, which is the core of the problem. Depending on the screen reader, the voice and the user's verbosity settings, an address may be read as a run of invented words and large numbers, spelled out one character at a time, or a mix of both. Users can always step through text character by character with their reading commands, but doing that for 40 characters is slow and easy to lose track of.

Truncated forms add their own trouble. The … character may be read as "ellipsis", as "dot dot dot", or not at all, depending on punctuation settings. When it is skipped, 0x71C7…976F can sound like one continuous string, and the listener has no idea that characters are missing.

Capitalization is another gap. When reading normally, screen readers generally don't say which letters are uppercase. Some signal it with a pitch change or the word "cap" when reading character by character, depending on settings, but listeners can't count on it. For Ethereum that is mostly harmless: the mixed case defined in EIP-55 is a checksum, and the same address in all lowercase points to the same account. For Base58 addresses, used by Solana and by older Bitcoin address formats, case is part of the value. A and a are different digits, so an address read aloud without case information can't be used to verify it.

Give truncated addresses a name that says what they are

The visible short form works as a recognizable label for sighted users. For screen reader users, say in words that it is a shortened address, and make the start and end clearly separate:

function Address({ address }: { address: string }) {
  const start = address.slice(0, 6)
  const end = address.slice(-4)
  return (
    <span className="address">
      <span aria-hidden="true">
        {start}…{end}
      </span>
      <span className="sr-only">
        Address starting {start}, ending {end}
      </span>
    </span>
  )
}

The visible text is hidden from assistive technology, and the visually hidden version replaces it. This pairing is more dependable than putting aria-label on the outer <span>. A plain span has no role, and ARIA doesn't support naming generic elements that way, so many screen readers ignore the label.

If you want the characters spelled out reliably, put spaces between them in the hidden text: 0 x 7 1 C 7. Most screen readers then read each character on its own. Even so, a lone lowercase "a" can come out as the word "a", so test with the screen readers your users have.

Interactive addresses need a slightly different approach. When the address is a link to an account page or a block explorer, or a button that opens details, keep the visible text in the accessible name and add context after it:

<a href="/accounts/0x71C7656EC7ab88b098defB751B7401B5f6d8976F">
  0x71C7…976F<span class="sr-only">, view account details</span>
</a>

That keeps the name consistent with what is on screen, which 2.5.3 Label in Name (A) asks of controls with visible text labels. It also gives voice control users a name that matches what they see.

Let people check the full address

Shortened addresses are fine for recognizing your own accounts. They are not enough for confirming where funds go. Address poisoning scams rely on exactly this: an attacker generates an address with the same first and last few characters as one you've used, then sends a tiny or zero-value transfer so the lookalike shows up in your history. Matching start and end characters is not proof, for anyone.

On send and confirmation screens, offer the full address in a form that is easier to follow, without breaking the value people copy:

function groupForReading(address: string) {
  const body = address.startsWith('0x') ? address.slice(2) : address
  const groups = body.match(/.{1,4}/g) ?? []
  return (address.startsWith('0x') ? ['0x', ...groups] : groups).join(' ')
}
// "0x 71C7 656E C7ab 88b0 98de fB75 1B74 01B5 f6d8 976F"

Show the grouped version inside a "Show full address" disclosure, and pair it with a copy button that copies the original, unspaced string. Groups of four are easier to step through by word, easier to compare against another screen and easier for sighted people to scan too. For Base58 addresses, comparing by ear isn't reliable because case can't be heard, so make copy and paste from a trusted source, and address book entries the user has named, the easy path. Displaying wallet addresses covers truncation lengths and layout in more detail.

Copy buttons that say what they copy

A copy button next to an address is usually just a clipboard icon, so it needs an accessible name. "Copy address" is clear. On a page with several addresses, such as a recipient, a token contract and your own account, add which one: "Copy recipient address", "Copy USDC contract address". A list of ten buttons all named "Copy" gives a screen reader user no way to tell them apart.

After copying, confirm it. Changing the button's label to "Copied" while it has focus is announced inconsistently, so send the confirmation through a status region instead:

<button type="button" onClick={() => copyAddress(address)}>
  <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16">{/* clipboard */}</svg>
  <span className="sr-only">Copy recipient address</span>
</button>
// rendered once per page, not once per button
<p role="status" className="sr-only">{message}</p>

copyAddress writes to the clipboard and then sets message to "Recipient address copied". One shared status region avoids dozens of live regions competing to speak. Icon-only buttons need names compares aria-label with hidden text, and copy to clipboard: browser rules and pitfalls covers making the copy itself reliable.

ENS names help, with care

An ENS name like alice.eth is far easier to hear, remember and say than a hex string. Where an address has a name, show the name as the main label and keep the shortened address beside it as secondary text, so both are available.

Two cautions apply. First, a reverse record (the "primary name" an address points to) can be set to any name, so before displaying it, resolve that name forward and check that it returns the same address. Second, names can include Unicode characters that look alike, so a name is a convenience, not a guarantee. Keep the full address one step away on any screen where money moves.

If you show an ENS avatar next to the name, it is decorative when the name is right there, so give it alt="".

A short test

Turn on VoiceOver, NVDA or TalkBack and go through your send flow without looking at the screen. You should be able to tell which addresses are shortened, identify each copy button by name, hear a confirmation after copying, and reach the full address of the recipient before confirming.

More in Accessibility

All guides