Designing Bunker for Phones, Screen Readers and Two Languages

A security tool helps only the people who can use it. Bunker shows which of your crypto addresses show their public key, and it gives a plan. If a visitor cannot read the page, that help does not reach them.

Picture three visitors on the same day. The first one holds a few coins and checks them on a small phone. The second one uses only a keyboard and a screen reader. The third one reads Spanish better than English.

All three need the same answer in about 30 seconds. None of them should need a course in cryptography to get it.

This article shows how I designed the page for all three. You will see the layout, the options that I rejected, the accessibility tests with their real results, and how one engine speaks two languages.


Who the page is for

The design notes of the project name three groups of users:

  • A crypto holder who read a warning and wants a fast answer.
  • A person or a team that checks many wallets at once.
  • A person who is not technical. The page must not need any crypto knowledge.

Many crypto holders also use a screen reader, a keyboard only, high zoom, a small phone or a second language. I expected most visitors to open the page on a phone. So every choice below follows two rules: give the answer fast, and never confuse the reader.

The order of the page

The tool comes first, and the explanations come after it. A visitor who knows the subject never scrolls past text to reach the scanner. This is the layout on a wide screen and on a phone:

Wide screen (900 pixels and more)            Phone
+------------------+-----------------------+  +---------------------+
| Headline         | Scanner: box, Scan,   |  | Headline, facts     |
| One sentence     | Load example, Clear   |  | Scanner             |
| Three facts      |                       |  | Result sentence     |
+------------------+-----------------------+  | Meter, legend       |
| Result sentence, meter, legend           |  | Address rows        |
+---------------------------+--------------+  | Plan                |
| Address rows              | Plan (stays  |  | Field guide         |
|                           | in view)     |  | Footer              |
+---------------------------+--------------+  +---------------------+
| Field guide: seven sections that open and close                    |
| Footer                                                             |
+--------------------------------------------------------------------+

The page is up to 1,200 pixels wide. It switches to two columns at 900 pixels. It keeps 16 pixels of space at the sides, and nothing scrolls sideways on a phone.

Eight design choices and the options I rejected

Each choice had two or more options. The project records all of them. This table gives the short form:

TopicOptionsChosenWhy
ResultsA table; one card for each addressCardsA card fits a phone and holds its own question and value field.
StatusColor only; icons only; a colored label with wordsWords with colorPeople with color blindness can read the words.
First viewAn empty page; example addresses already scannedExample dataThe visitor sees what the tool does before they paste anything.
Name for the public keyTechnical words; the word “lock” with a glossary“Lock”It is easy to remember, and it has one meaning everywhere.
SummaryThree number tiles; one sentence and a meterSentence and meterThe answer comes in words. The meter shows the share at a glance.
Plan positionBelow all results; next to the results on a wide screenNext to the resultsEach answer changes the plan in view.
Reference textAll sections open; tabs; native sections that open and closeNative sectionsThe page stays short, and the sections work with no script.
Keep the planDownload a file; copy as text; printCopy and printCopy works in every viewer. Print gives a PDF.

Four of these choices need an example.

Words first, color second

Bunker calls the public key the lock. Each result shows a colored edge, an icon and words. Red means “Lock visible”, amber means “Check needed”, and green means “Lock hidden”. The icons are an open lock, a closed lock and a question mark.

A visitor who cannot tell red from green still reads “Lock visible” on the card. The color supports the words. The words carry the meaning.

The first view is a finished scan

An empty box shows nothing, so the visitor does not know what to expect. Bunker opens with five public example addresses, already scanned. A blue banner says: “Example data. These are public test addresses with made-up values.”

LabelNetworkExample answerExample valueResult
OrdinalsBitcoin (Taproot)No question$3,100Lock visible
BTC savingsBitcoin (Native SegWit)No$9,800Lock hidden
Daily walletEVM networksYes$6,400Lock visible
SOL stakingSolanaNo question$2,200Lock visible
DOTPolkadotNo question$750Lock visible

Above the cards, the page writes this sentence:

From the result sentence of the example:

4 of 5 addresses show their lock. $12,450 sits behind a visible lock.

The $12,450 is the sum of the four visible values: 3,100 plus 6,400 plus 2,200 plus 750. The savings address answered “No”, so its $9,800 stays out of the total.

One sentence and one meter

Three number tiles make the reader add the numbers. Bunker writes the answer as a sentence instead. Below it, a meter shows one segment for each address, in the status colors.

The meter is the only motion on the page. It fills once after each scan. If the visitor turns on “reduce motion” in the system settings, the meter does not animate. The style sheet plays the animation only under prefers-reduced-motion: no-preference.

A field guide that opens and closes

The explanations sit below the tool, in seven sections. Each section uses the native HTML details element. It opens with a click or a key, and it works with no script.

The first section, “Where things stand on October 25, 2026”, is open. The others are Why extra security now, Supported networks, Words you will see, Use Bunker with AI agents, Accessibility, and Legal notice.

The target: WCAG 2.2 at Level AA

WCAG (Web Content Accessibility Guidelines) is the shared standard for accessible web pages. Version 2.2 is also the international standard ISO/IEC 40500:2025. The European standard EN 301 549 uses it.

OptionForAgainst
A. No targetNo workUsers find the problems, not the tests.
B. An accessibility overlay widgetFast to addIt does not fix the page. It can get in the way of the visitor’s own screen reader. The Content Security Policy blocks its script.
C. WCAG 2.1 AAWidely usedIt misses focus not obscured, target size and redundant entry.
D. WCAG 2.2 AA, with automated and manual tests, a statement and a contactThe current standard; it covers 2.1Manual tests take time at each release.

I chose D. The page has an accessibility statement in the field guide. It gives the target, the test method, the known limits, the date of the last review and a contact. It promises an answer within 5 working days, and it makes no claim of conformance.

The audit and its real results

The automated audit uses axe-core 4.14.0, an open-source accessibility test engine. The script audit/a11y-audit.cjs opens the page in Chromium four times. Before each run, it opens every field guide section, so the hidden text is tested too.

LanguageThemeWidthViolationsPassed rules
EnglishLight1,280 px050
EnglishDark1,280 px050
SpanishLight390 px049
SpanishDark390 px049

The rules cover WCAG 2.0, 2.1 and 2.2 at levels A and AA, plus best practices. axe-core runs only in the test browser. It is not in the page, and it is not a dependency of the project.

An automated tool cannot test everything. These checks were done by hand or by script:

WCAG criterionCheckResult
2.1.1 KeyboardComplete the whole flow with Tab, Shift+Tab, Enter and Space.Pass
2.4.1 Bypass blocks“Skip to the scanner” is the first item.Pass
2.4.7 and 2.4.11 FocusA 2-pixel outline on every control. No sticky element covers it.Pass
1.4.10 ReflowA width of 320 pixels in both languages, with no sideways scroll.Pass
1.4.12 Text spacingLine height 1.5, letter spacing 0.12 em, word spacing 0.16 em. No cut text.Pass
2.5.8 Target sizeEvery button and link is at least 24 by 24 pixels.Pass
3.1.2 Language of partsSpanish blocks have lang="es", and the page language changes with the switch.Pass
3.3.7 Redundant entryThe language and theme switches keep the pasted addresses.Pass
4.1.3 Status messagesThe result sentence uses role="status". The secret alert uses role="alert".Pass

The project also records its limits. A screen reader reads a long address one character at a time, because that is the nature of an address. For that reason, the plan is also available as plain text, with Copy plan and from the command-line tool. The checks used Chromium. Checks in Firefox and Safari, and a full review with NVDA, VoiceOver and TalkBack, are the next steps.

Three problems that the checks found

P-25: keyboard focus went back to the top

An answer such as “Yes” rebuilds the address rows. The rebuild removed the button that had keyboard focus. A keyboard user then went back to the top of the page after every answer.

There were two options. The code could change the rows in place, or it could rebuild them and then put focus back. In-place changes add more code paths that can get out of sync. I chose the rebuild, with one render path. This is the fix in src/ui/app.js:

    uiRender();
    // The rows are rebuilt, so put keyboard focus back where it was (P-25).
    const again = act === 'remove' ? null
      : cards.querySelector('[data-id="' + CSS.escape(e.id) + '"] [data-act="' + CSS.escape(act || '') + '"][data-v="' + CSS.escape(v) + '"]');
    /** @type {HTMLElement} */ (again || uiById('results')).focus({ preventScroll: true });

Read it in this order:

  1. uiRender() draws all rows again from the page state.
  2. The code looks for the same button in the same row. It matches the row ID (data-id), the action (data-act) and the value (data-v).
  3. CSS.escape makes each value safe to use inside the search pattern.
  4. If the visitor removed the row, no such button exists. Focus then goes to the results area.
  5. preventScroll: true keeps the page where it is.

For example, you press Enter on “No” on the savings card. The card changes to “Lock hidden”, and focus stays on “No”.

P-29: borders and one link were below the minimum

The border of the address box had a contrast of about 1.4:1. WCAG 1.4.11 asks for 3:1 for the parts that show a control. The “Open in explorer” link was 23 pixels high, and WCAG 2.5.8 asks for 24.

The fix is a new color token, --field-line, with a contrast of 3:1 or more in both themes. The address box, the value field, the answer buttons and the header switches use it. Links and small buttons now have a minimum height of 24 pixels.

P-30: an English name for a Spanish region

The headline section used aria-labelledby, an attribute that names a page region after another element. It pointed at the English headline. In Spanish, that headline is hidden, but a screen reader still reads an element that aria-labelledby names.

So Spanish users heard the English headline as the name of the region. One option was to change the attribute with the language. I removed the region name instead. The headline is already the first heading, so the reader loses nothing.

Two languages, one engine

Many Bunker users speak Spanish, in Latin America, in Spain and in the US. The engine writes about 200 sentences: explanations, plan steps and labels. I considered four ways to give them a second language:

OptionAgainst
A. English onlyBrowser translation can change the meaning of a security instruction.
B. Two copies of the pageTwo builds to keep the same. The switch reloads the page and loses the pasted addresses.
C. Message keys in the engineEvery detector changes, and the English text moves away from the code that it describes.
D. The engine writes English, and the page translates what it showsThe dictionary must follow every English change.

I chose D. The engine and its tests stay the same. A reviewer sees the English and the Spanish side by side. The switch is instant, and it keeps the input. This is the translation function in src/i18n/translate.js:

export function toSpanish(text) {
  if (!text) return text;
  const whole = esOne(text.trim());
  if (whole !== null) return whole;
  return sentences(text.trim()).map(s => {
    const t = esOne(s);
    return t === null ? s : t;
  }).join(' ');
}

The helper esOne() looks up one piece of text. It tries the sentence dictionary, then the label dictionary, then a list of patterns. With that helper, the function works in three steps:

  1. It tries the whole text as one sentence or one label.
  2. If that fails, it splits the text into sentences.
  3. It translates each sentence. A sentence with no translation stays in English.

Step 3 sounds like a gap, but the tests close it. They fail the build if any engine text stays in English. Here is the code at work. I scanned the same Bitcoin address in both languages with the command-line tool:

FieldEnglishSpanish (--lang es)
explanationOnly a hash of the key is on chain until this address spends.Solo un hash de la clave está en la cadena hasta que esta dirección gaste.
questionHas this address ever sent a transaction or signed a message? Answer yes or no.¿Esta dirección alguna vez envió una transacción o firmó un mensaje? Responde sí o no.

The plan comes out in Spanish too. These are the first lines of the real output:

$ node cli/bunker.js --text --lang es --sent bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4=no addresses.txt
PLAN BUNKER

1. Reglas básicas
   - No te apresures. Una mala migración puede costar más que el riesgo que intentas evitar.
   - No escribas una frase semilla ni una clave privada en un sitio web ni en un chat. Las herramientas reales no la piden.
   - No sigas enlaces que te digan "migra ahora". Usa solo tu propia app de billetera.
   - Ledger y Trezor no envían un "parche cuántico" por correo. Actualiza el firmware solo en Ledger Live o Trezor Suite.
   - Envía un monto pequeño de prueba antes de cada movimiento grande.

2. Mantén ocultos estos candados
   - bc1qw508...v8f3t4 (Bitcoin | Ahorros): Úsala solo para recibir. Cuando gastas, tu billetera envía el cambio a una dirección nueva y el resto queda oculto.

The Spanish uses “tú”, the normal form in modern Latin American products. One word keeps one meaning everywhere: “candado” is the lock, and “billetera” is the wallet.

The file test/i18n.test.js holds 8 tests. They check every engine sentence, every result for the real test addresses and their typos, every plan step, and the page text in both languages. On October 27, 2026, all 8 passed.

P-27: the font had no Spanish letters

The page embeds one open-license font, Archivo. The first cut of the font kept only Basic Latin. Spanish needs á, é, í, ó, ú, ñ, ¿ and ¡, so the browser drew those letters with a different font. Words looked mixed.

The full font was about 650 KB. A new cut with the Latin-1 Supplement has every Spanish letter and is 74 KB. I chose the new cut, and the build now checks its new SHA-256 hash.

P-28: a pattern that crossed two sentences

An Ethereum card lists the networks that share one key. The real text names 27 networks, then adds a second sentence about tokens. Both sentences contain the word “and”.

The pattern “Same key on (list) and (last).” ran on the whole text first. It took the “and” from the second sentence, so half of the text stayed in English. The test file found the problem.

The fix is one rule: a part of a pattern cannot cross a period. In src/i18n/es-labels.js, each part uses [^.]+, which means “any characters except a period”. A real run of the function shows the result:

toSpanish('Same key on Ethereum, Base and Arbitrum.')
-> 'La misma clave en Ethereum, Base y Arbitrum.'

Preferences that store nothing

Visitors asked for a light and dark switch and an English and Spanish switch. Most sites remember these choices. Bunker promises that it stores nothing on the device, and a test fails if the code uses browser storage.

OptionForAgainst
A. Save the choice in browser storage or a cookieThe choice stays for the next visit.It breaks the promise. A cookie needs a consent notice in the EU.
B. Start from the system settings, and keep the choice in memory onlyThe promise stays true.A visitor who picks another theme picks it again next time.
C. Put the language in the address (#es)A link can open the page in Spanish.It works only for the language.

I chose B, with C for the language. The page works like this:

  • Language. If the address ends with #es or #en, the page uses it. If not, the page uses the first language of the browser.
  • Link. The switch writes #es into the address, so you can share the Spanish page. The browser never sends this part of the address to the server.
  • Theme. The page follows the color setting of the system. The button changes the theme for this visit only.
  • Labels. The theme button says what it does, for example “Switch to dark mode”. It does not name the current state.

For example, open bunker.nosistech.com/#es. The page starts in Spanish. Paste an address, switch to English, and the address is still in the box.

Read the design records

The next and last article of the series is the build log. It covers the whole project in order, from the first prototype to version 1.2.1, with the 42 problems and the 30 decisions.

Have a question or feedback about Bunker? Contact NosisTech.

Keep reading: Bunker by NosisTech

Previous article: Bunker for AI Agents: A Local CLI and MCP Server With No Web API

Next article: The Bunker Build Log: 42 Problems Fixed and 30 Decisions Recorded

All articles: NosisTech Writing

This post is general information, not financial, legal or security advice. Bunker is an educational tool with no warranty. Examine each address on a block explorer, and send a small test amount before you move funds.

WEEKLY BUILD NOTES

One documented agent build in your inbox, every week

Real systems, real code, the errors left in. No spam, unsubscribe anytime.