Designing frontends that can keep up with mainnet.
Introduction
A good blockchain explorer looks simple: a search bar, a list of blocks, transaction details, a few charts. Underneath, it sits on top of:
[ node(s) ] -> [ indexer + APIs ] -> [ explorer frontend ]
The frontend has to hide protocol complexity, tolerate reorgs, partial data, and node hiccups, and still feel fast on low‑end devices.
In this article I’ll walk through how I design React + Material UI explorers that stay responsive at chain head, based on building explorers for multiple EVM and non‑EVM chains. I’ll stay frontend‑centric and focus on architecture, not on a particular React state library or router.
Prerequisites & assumptions
I assume:
- you’re comfortable with modern React (function components, hooks); (UXPin)
- you’re using TypeScript (strongly recommended to tame blockchain types);
- you already have an indexer / backend that exposes REST, GraphQL, or WebSocket endpoints.
I’ll use React and Material UI terminology but avoid full code dumps. The goal is to give you a mental model you can adapt to your stack.
1. Explorer requirements from the UI point of view
Stripped of branding, most explorers expose the same surfaces:
- Global search (address / tx / block / token / pool)
- Blocks (latest N, pagination, details)
- Transactions (per block, per address, per contract)
- Addresses / accounts (summary, balances, history)
- Contracts / pools (overview, events, analytics)
- Network status (height, TPS, gas/fees, validator health)
And they must respect a few UX constraints:
- high-frequency updates near chain head
- deep-linkable, shareable URLs
- fast cross-navigation between related entities
- readable typography in dense tables
- good behaviour on mobile and low-end hardware
This is the baseline. Any extra feature (charts, dashboards, NFTs, DeFi widgets) should not break these fundamentals. Explorers like Etherscan and friends evolved in exactly this direction: solid core views first, specialised features later. (Token Metrics)
2. High‑level frontend architecture
The frontend has three layers in my head:
+-----------------------------+
| Node / RPC / Indexer |
| (your backend) |
+-----------------------------+
|
HTTP / WebSocket
|
v
+-----------------------------+
| Data layer in React |
| - API clients |
| - hooks / caches |
| - subscriptions |
+-----------------------------+
|
v
+-----------------------------+
| UI layer (React + MUI) |
| - routing |
| - page components |
| - tables, cards, charts |
+-----------------------------+
Even when there is “just one backend service”, I keep a clean separation between:
- data layer (API clients, hooks, WebSocket handling, caching),
- page layout and routing (entity‑centric routes),
- presentational components (tables, cards, chips, status bars).
That separation is what will save you when you add a second chain, a second backend, or when you refactor your indexer.
3. UI skeleton with Material UI
Material UI gives you most of the atomic pieces for an explorer UI: AppBar, Toolbar, Drawer, Container, Grid, Table, Tabs, Chip, Skeleton, and a theming system that works across light/dark. (MUI)
A layout I reuse a lot:
+----------------------------------------------------------+
| AppBar: logo | network selector | global search |
+----------------------------------------------------------+
| (optional) permanent / responsive Drawer |
| |
| +----------------------------------------------------+ |
| | Status bar: height, lag, gas/fees, validators | |
| +----------------------------------------------------+ |
| | Main content: blocks / tx / address / contract | |
| +----------------------------------------------------+ |
+----------------------------------------------------------+
| Footer: links, version, network docs |
+----------------------------------------------------------+
Key points:
- On desktop, the drawer can be permanent; on mobile, it becomes temporary and slides in (MUI’s responsive drawer pattern gives you this out of the box). (MUI)
- A global status bar near the top (height, gas, indexer lag) is worth the pixels; it gives users a constant sense of “how live” the view is.
- Hashes, addresses, and numeric values should use monospace fonts for scanability.
I don’t try to be too inventive with layout. Explorers are information‑dense tools; clarity and consistency beat creativity.
4. Routing and URL design
Explorers are essentially a graph of detail pages. I design routes early and make components follow routing, not the other way around.
Typical routes:
/ -> dashboard or latest blocks
/block/:heightOrHash
/tx/:hash
/address/:id
/contract/:id
/token/:id
/search?q=...
/validators
/pools
/status
In a multi‑chain explorer I add the chain as a prefix or query:
/:chainId/block/:heightOrHash
/:chainId/address/:id
Rules I keep:
- every core entity has a stable, sharable URL;
- the URL is the source of truth for what is being displayed (not some hidden “selected item” state);
- back/forward buttons always do something intuitive (because state is reflected in the route).
Inside pages, I use tabs or query params for secondary views:
/address/:id?tab=txs
/address/:id?tab=internal-txs
That keeps the routing tree shallow but still gives users shareable “sub‑views”.
5. Data fetching and real‑time updates
Explorers deal with two very different data profiles:
-
Slow‑moving or static Chain metadata, token metadata, contract ABIs, verified source, documentation links. These can be fetched once and cached aggressively.
-
Hot data near chain head Latest blocks, recent transactions, mempool snapshots, validator health, gas/fee levels. These need continual updates.
I design the data layer explicitly around this split.
5.1 Fetching and caching
For static / slowly changing data:
- cache in memory via hooks or a query library,
- cache on the backend as well (don’t put ABIs on the hot path),
- revalidate on page focus or on explicit refresh.
For example, a useBlock(height) hook can:
- fetch once,
- keep data cached by height,
- only refetch if the block is very recent or marked “unfinalised”.
React hook rules still apply: use* hooks at the top of components, stable dependencies, and no hooks in loops or conditionals. (React)
5.2 Real‑time updates (WebSockets / SSE)
For “live” data, I prefer push‑based updates over aggressive polling:
- WebSocket or server‑sent events for “new block” notifications; (Ably Realtime)
- a small client‑side queue of “recent blocks” or “recent txs”;
- a shared subscription layer (one WebSocket connection, many subscribers in the UI).
Flow:
[ WebSocket "new block" ]
|
v
[ global recentBlocks store ]
| | |
v v v
Blocks page Status bar "New blocks" toast
I avoid auto‑scrolling the user away from what they’re reading. Instead:
- if the user is on the latest blocks page and scrolled to the top, new blocks can be appended automatically;
- otherwise, show a “New blocks available – click to refresh” bar.
Reorgs are mostly a backend contract, but the frontend must tolerate:
- a block’s confirmation count changing over time;
- a transaction’s status switching from “pending” → “confirmed” → “failed”;
- very occasionally, a block being replaced entirely.
For this, I treat a block as “live” for a configurable number of confirmations and allow periodic refresh or push updates. Beyond that threshold it’s just historical data.
6. Search experience
The search bar is the fastest way users judge whether your explorer “gets it”.
I break search into three concerns.
6.1 Input detection
Given a raw string, I try to infer intent:
- looks like a tx hash -> /tx/:hash
- looks like a block height -> /block/:height
- looks like an address -> /address/:id
- otherwise -> /search?q=...
Patterns differ per chain (hex vs bech32 vs base58 vs human‑readable names). I keep the parsing logic in a small, well‑tested utility module and surface inline hints in the UI:
“Looks like a transaction hash – press Enter to go to the transaction page.”
6.2 Navigation and fallbacks
Behaviour I aim for:
- if the input unambiguously matches a single entity → redirect to that entity’s page;
- if it may match several types → land on a
/searchresults page with grouped sections:
Results for "foo":
Transactions (2)
Blocks (0)
Addresses (1)
Tokens (3)
Pools (0)
6.3 Performance and rate limiting
I debounce search queries on the client and — more importantly — ensure the backend has a single search endpoint that can handle all entity types. The frontend should not fire five requests per keystroke.
When you hit rate limits or backend errors, the search box should:
- degrade gracefully (“search temporarily unavailable”),
- avoid spamming retries,
- never lock up the entire header.
7. Tables, virtualization, and pagination
Address and contract pages can easily involve tens of thousands of transactions. Rendering them all at once is how you turn a browser into a space heater.
I use three layers of defence.
7.1 Server‑side pagination with cursors
Explorers are almost always backed by cursor‑based pagination rather than offset/limit:
GET /address/:id/txs?cursor=&limit=50
The frontend’s job is to:
- render one page at a time,
- show “Previous / Next” or “Load more”,
- keep the current cursor in the URL or component state.
Cursor‑based pagination is essential for correctness on moving chains (no gaps or duplicates when new txs arrive between requests).
7.2 Incremental loading UX
Numeric pagination (page=1..N) is rarely how people explore activity. On high‑activity addresses, I prefer an infinite‑scroll or “Load more” pattern that:
- keeps the DOM bounded (e.g. only a few hundred rows),
- gives users a sense of continuous history,
- plays nicely with browser back/forward behaviour.
7.3 Virtualization (when it’s really needed)
For certain heavy views (e.g. global “all txs” stream), I’ll add table virtualization so only visible rows are in the DOM. This is a powerful but sharp tool:
- it breaks naïve assumptions about row height and scrolling;
- it complicates components like sticky headers and “scroll to row”.
I only introduce virtualization where profiling shows that a plain paginated table isn’t enough.
On mobile I almost always switch from tables to stacked cards for transactions and blocks; virtualization rarely buys much there.
8. Theming and blockchain‑specific UX
Material UI theming gives you global control over colours, typography, and density. Explorers greatly benefit from a data‑centric theme:
- light and dark modes with sufficient contrast
- consistent colour semantics (success/warn/error/info)
- clear shapes for clickable vs static elements
- compact but readable typography for tables
Blockchain brings its own UX expectations:
- transparency (hashes, timestamps, confirmations, fees); (ProCreator)
- trust signals (network status, data freshness, verification badges);
- different personas (retail users vs power users, devs vs traders). (ecinnovations.com)
Practical touches:
- colour‑code networks clearly (mainnet vs testnet vs devnet),
- highlight “verified contracts” distinct from unknown ones,
- show confirmations and an “unconfirmed” badge rather than hiding uncertainty,
- surface fees and gas in both native units and fiat where appropriate.
Blockchain UX literature keeps repeating the same point: clarity over cleverness. Users are often dealing with irreversible actions and real money; your explorer’s job is to make the state of the world legible. (ProCreator)
9. Error states and partial failure
On-chain systems and indexers fail in interesting ways:
- node RPCs time out or rate‑limit you;
- indexers fall behind the tip;
- specific resources are not yet indexed.
Your UI must distinguish:
- "data is loading"
- "data is missing *because it doesn't exist*"
- "data is missing *because the indexer is behind*"
- "data is temporarily unavailable"
I rely on:
-
Skeletons for initial load, not white screens;
-
inline banners for degraded modes:
- “Data indexed to block N, chain head N+K”
- “Logs for this block are still being indexed”
-
explicit “Try again” actions on failure, not just spinner loops;
-
per‑page, not global, error handling wherever possible.
The goal is to never pretend everything is fine when your data is clearly incomplete.
10. Performance and bundling
Explorers can get heavy quickly: lots of components, charts, rich tables.
A short checklist I actually use:
- Code-split heavy sections (analytics, charts, admin) away from the core flows.
- Avoid doing large JSON parsing or hex decoding in render paths; push it to the backend where possible.
- Keep global state minimal; store derived data close to where it’s rendered.
- Memoise expensive derived views (e.g. aggregation of txs into charts).
- Use browser devtools and React Profiler; don’t optimise by folklore.
On the bundling side:
- pick one charting library and stick to it;
- avoid giant “kitchen sink” component libraries on top of MUI;
- regularly inspect bundle size and dependency bloat.
On fast machines the difference might be small; on low‑end hardware or mobile browsers it decides whether power users recommend your explorer to others.
11. Experience callouts
From the trenches – multi‑chain. When you add a second chain, don’t fork the entire frontend. Treat
chainIdas a first‑class dimension in routing, theming, and data hooks. One explorer with a network selector and chain‑aware components is far easier to evolve than two divergent codebases that accidentally diverge in UX and features.
From the trenches – live head behaviour. On a fast EVM chain we initially auto‑scrolled the “latest blocks” view and live‑updated everything. Traders hated it: the UI kept jumping while they inspected specific blocks. Switching to a fixed list with a “New blocks” toast and a manual refresh button made the app feel calmer without losing liveness.
From the trenches – trust and honesty. Users notice very quickly if your explorer hides lag or errors. We had a case where the indexer was 20+ minutes behind under load, but the UI didn’t show it. Support tickets exploded. After we added a simple “Synced to block N / head N+K” banner and treated that as a first‑class state, complaints dropped sharply even when lag reappeared. People will forgive latency; they won’t forgive being misled.
Conclusion
A blockchain explorer frontend is not “just some React tables on top of an API”.
It is a dense, real‑time interface over a noisy, partially ordered dataset, with real expectations around accuracy, liveness, and clarity.
If you:
- design your routes and URLs around core entities,
- keep a clean data layer with push-based updates where it matters,
- use Material UI to build a clear, responsive layout,
- respect browser limits with pagination and occasional virtualization,
- treat degraded modes and lag as first-class UX states,
- and keep blockchain-specific trust signals visible at all times,
you end up with an explorer that feels reliable even when the network and nodes are not. Users very quickly learn to treat it as the source of truth for the chains you support—and that’s the bar I aim for whenever I ship an explorer frontend.
References & further reading
- React official docs – hooks, rules of hooks, best practices. (React)
- Material UI docs – AppBar, Drawer, layout and theming patterns. (MUI)
- WebSockets with React – patterns for real‑time updates in React apps. (Ably Realtime)
- Etherscan and explorer UX – examples of explorer features and UI expectations. (Token Metrics)
- Blockchain UX design – best practices for clarity, trust, and multi‑persona interfaces. (ProCreator)
- ENS Front‑end design guidelines – good general guidance for Web3 UIs. (ENS Documentation)
::contentReference[oaicite:15]{index=15}