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:
| Topic | Options | Chosen | Why |
|---|---|---|---|
| Results | A table; one card for each address | Cards | A card fits a phone and holds its own question and value field. |
| Status | Color only; icons only; a colored label with words | Words with color | People with color blindness can read the words. |
| First view | An empty page; example addresses already scanned | Example data | The visitor sees what the tool does before they paste anything. |
| Name for the public key | Technical words; the word “lock” with a glossary | “Lock” | It is easy to remember, and it has one meaning everywhere. |
| Summary | Three number tiles; one sentence and a meter | Sentence and meter | The answer comes in words. The meter shows the share at a glance. |
| Plan position | Below all results; next to the results on a wide screen | Next to the results | Each answer changes the plan in view. |
| Reference text | All sections open; tabs; native sections that open and close | Native sections | The page stays short, and the sections work with no script. |
| Keep the plan | Download a file; copy as text; print | Copy and print | Copy 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.”
| Label | Network | Example answer | Example value | Result |
|---|---|---|---|---|
| Ordinals | Bitcoin (Taproot) | No question | $3,100 | Lock visible |
| BTC savings | Bitcoin (Native SegWit) | No | $9,800 | Lock hidden |
| Daily wallet | EVM networks | Yes | $6,400 | Lock visible |
| SOL staking | Solana | No question | $2,200 | Lock visible |
| DOT | Polkadot | No question | $750 | Lock 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.
| Option | For | Against |
|---|---|---|
| A. No target | No work | Users find the problems, not the tests. |
| B. An accessibility overlay widget | Fast to add | It 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 AA | Widely used | It misses focus not obscured, target size and redundant entry. |
| D. WCAG 2.2 AA, with automated and manual tests, a statement and a contact | The current standard; it covers 2.1 | Manual 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.
| Language | Theme | Width | Violations | Passed rules |
|---|---|---|---|---|
| English | Light | 1,280 px | 0 | 50 |
| English | Dark | 1,280 px | 0 | 50 |
| Spanish | Light | 390 px | 0 | 49 |
| Spanish | Dark | 390 px | 0 | 49 |
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 criterion | Check | Result |
|---|---|---|
| 2.1.1 Keyboard | Complete 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 Focus | A 2-pixel outline on every control. No sticky element covers it. | Pass |
| 1.4.10 Reflow | A width of 320 pixels in both languages, with no sideways scroll. | Pass |
| 1.4.12 Text spacing | Line height 1.5, letter spacing 0.12 em, word spacing 0.16 em. No cut text. | Pass |
| 2.5.8 Target size | Every button and link is at least 24 by 24 pixels. | Pass |
| 3.1.2 Language of parts | Spanish blocks have lang="es", and the page language changes with the switch. | Pass |
| 3.3.7 Redundant entry | The language and theme switches keep the pasted addresses. | Pass |
| 4.1.3 Status messages | The 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:
uiRender()draws all rows again from the page state.- 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). CSS.escapemakes each value safe to use inside the search pattern.- If the visitor removed the row, no such button exists. Focus then goes to the results area.
preventScroll: truekeeps 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:
| Option | Against |
|---|---|
| A. English only | Browser translation can change the meaning of a security instruction. |
| B. Two copies of the page | Two builds to keep the same. The switch reloads the page and loses the pasted addresses. |
| C. Message keys in the engine | Every 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 shows | The 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:
- It tries the whole text as one sentence or one label.
- If that fails, it splits the text into sentences.
- 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:
| Field | English | Spanish (--lang es) |
|---|---|---|
| explanation | Only 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. |
| question | Has 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.
| Option | For | Against |
|---|---|---|
| A. Save the choice in browser storage or a cookie | The 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 only | The 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
#esor#en, the page uses it. If not, the page uses the first language of the browser. - Link. The switch writes
#esinto 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 tool: bunker.nosistech.com
- Every design choice with its options: docs/07-ux-ui.md
- The audit, the method and the results: docs/11-accessibility.md
- The decisions: ADR-028, ADR-029 and ADR-030
- The code: github.com/nosistech/nosis-bunker
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.