Web3 — Network / Chain Switcher
A glassy Web3 network switcher with a current-chain pill that opens a searchable menu of fictional chains — Ethereum, Arbitrum, Base, Optimism, Polygon and Lumen Chain — each with a colored icon, online or degraded status dot, and a collapsible testnet section. Selecting a chain triggers a simulated Switching network state, then updates the pill, the hero card with animated block count, monospace gas and RPC values, and retints the whole page accent to match the active network.
MCP
Kod
:root {
--bg: #0a0b0f;
--surface: #13151c;
--surface-2: #1b1e27;
--elevated: #23262f;
--text: #e9ecf2;
--muted: #8a90a2;
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--accent: #7c5cff;
--accent-2: #00e0c6;
--accent-glow: rgba(124, 92, 255, 0.45);
--pos: #26d07c;
--neg: #ff4d6d;
--warn: #ffb347;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-pill: 999px;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: "Space Grotesk", system-ui, sans-serif;
line-height: 1.5;
position: relative;
overflow-x: hidden;
transition: background 0.5s ease;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-variant-numeric: tabular-nums;
}
.bg-orb {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(620px 420px at 82% -8%, var(--accent-glow), transparent 70%),
radial-gradient(520px 360px at 4% 108%, rgba(0, 224, 198, 0.16), transparent 72%);
transition: background 0.55s ease;
}
.shell {
position: relative;
z-index: 1;
max-width: 760px;
margin: 0 auto;
padding: 28px 22px 56px;
}
/* ── Topbar ───────────────────────────── */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 30px;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
}
.brand-mark {
width: 26px;
height: 26px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
box-shadow: 0 0 18px var(--accent-glow);
}
.brand-name {
font-weight: 700;
letter-spacing: -0.01em;
}
.brand-tag {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
border: 1px solid var(--line);
padding: 2px 6px;
border-radius: var(--r-pill);
}
.topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
/* ── Switcher / pill ──────────────────── */
.switcher {
position: relative;
}
.chain-pill {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 11px 7px 8px;
background: linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(135deg, var(--line-2), transparent) border-box;
border: 1px solid transparent;
border-radius: var(--r-pill);
color: var(--text);
font-family: inherit;
font-size: 13px;
cursor: pointer;
backdrop-filter: blur(10px);
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.chain-pill:hover {
background: var(--surface-2);
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.35);
}
.chain-pill:active {
transform: scale(0.98);
}
.switcher.open .chain-pill {
box-shadow: 0 0 0 1px var(--accent-glow), 0 8px 26px rgba(0, 0, 0, 0.45);
}
.chain-ico {
width: 26px;
height: 26px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 12px;
color: #0a0b0f;
background: var(--accent);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14) inset, 0 0 14px -2px var(--accent-glow);
flex-shrink: 0;
}
.ico-glyph {
font-family: "Space Grotesk", sans-serif;
}
.pill-text {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.1;
}
.pill-name {
font-weight: 600;
}
.pill-net {
font-size: 10px;
color: var(--muted);
letter-spacing: 0.02em;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.is-online {
background: var(--pos);
box-shadow: 0 0 0 3px rgba(38, 208, 124, 0.18);
}
.status-dot.is-degraded {
background: var(--warn);
box-shadow: 0 0 0 3px rgba(255, 179, 71, 0.18);
}
.chev {
width: 16px;
height: 16px;
color: var(--muted);
transition: transform 0.2s ease;
}
.switcher.open .chev {
transform: rotate(180deg);
}
/* ── Menu ─────────────────────────────── */
.menu {
position: absolute;
top: calc(100% + 10px);
right: 0;
width: 312px;
max-width: calc(100vw - 32px);
background: rgba(19, 21, 28, 0.82);
backdrop-filter: blur(20px) saturate(140%);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: 0 24px 60px -12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
padding: 14px;
z-index: 30;
transform-origin: top right;
animation: pop 0.18s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
@keyframes pop {
from { opacity: 0; transform: translateY(-8px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.menu-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
}
.menu-title {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.menu-hint {
font-size: 11px;
color: var(--muted);
}
.search {
position: relative;
display: flex;
align-items: center;
margin-bottom: 10px;
}
.search-ico {
position: absolute;
left: 11px;
width: 15px;
height: 15px;
color: var(--muted);
pointer-events: none;
}
.search-input {
width: 100%;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
color: var(--text);
font-family: inherit;
font-size: 13px;
padding: 9px 44px 9px 33px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.search-input::placeholder {
color: var(--muted);
}
.search-input:focus-visible {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.search-kbd {
position: absolute;
right: 9px;
font-family: "JetBrains Mono", monospace;
font-size: 10px;
color: var(--muted);
border: 1px solid var(--line);
border-radius: 5px;
padding: 1px 5px;
background: var(--elevated);
}
.chain-list {
list-style: none;
margin: 0;
padding: 2px;
display: flex;
flex-direction: column;
gap: 2px;
max-height: 268px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--line-2) transparent;
}
.chain-list::-webkit-scrollbar { width: 6px; }
.chain-list::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 99px; }
.chain-row {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
padding: 9px 10px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--r-md);
color: var(--text);
font-family: inherit;
font-size: 13px;
text-align: left;
cursor: pointer;
transition: background 0.13s ease, border-color 0.13s ease;
}
.chain-row:hover,
.chain-row:focus-visible {
background: var(--surface-2);
outline: none;
}
.chain-row:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-glow);
}
.chain-row.is-active {
background: linear-gradient(90deg, rgba(124, 92, 255, 0.14), transparent);
border-color: var(--line-2);
}
.row-ico {
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 13px;
color: #0a0b0f;
flex-shrink: 0;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.14) inset;
}
.row-body {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.row-name {
font-weight: 500;
display: flex;
align-items: center;
gap: 7px;
}
.row-tag {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warn);
border: 1px solid rgba(255, 179, 71, 0.4);
border-radius: var(--r-pill);
padding: 1px 6px;
}
.row-sub {
font-size: 11px;
color: var(--muted);
}
.row-end {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.row-status {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 0 3px rgba(38, 208, 124, 0.16);
}
.row-status.is-degraded {
background: var(--warn);
box-shadow: 0 0 0 3px rgba(255, 179, 71, 0.16);
}
.check {
width: 16px;
height: 16px;
color: var(--accent-2);
opacity: 0;
transform: scale(0.6);
transition: opacity 0.15s ease, transform 0.15s ease;
}
.chain-row.is-active .check {
opacity: 1;
transform: scale(1);
}
.empty {
text-align: center;
color: var(--muted);
font-size: 12px;
padding: 18px 0 6px;
margin: 0;
}
.testnet-bar {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.switch {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
cursor: pointer;
}
.switch-label {
display: flex;
flex-direction: column;
}
.switch-title {
font-size: 13px;
font-weight: 500;
}
.switch-sub {
font-size: 11px;
color: var(--muted);
}
.switch-input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.switch-track {
width: 42px;
height: 24px;
border-radius: var(--r-pill);
background: var(--elevated);
border: 1px solid var(--line-2);
position: relative;
flex-shrink: 0;
transition: background 0.2s ease;
}
.switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.2s cubic-bezier(0.2, 0.9, 0.3, 1.2), background 0.2s ease;
}
.switch-input:checked + .switch-track {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
border-color: transparent;
}
.switch-input:checked + .switch-track .switch-thumb {
transform: translateX(18px);
background: #fff;
}
.switch-input:focus-visible + .switch-track {
box-shadow: 0 0 0 3px var(--accent-glow);
}
/* ── Wallet pill ──────────────────────── */
.wallet-pill {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 13px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-pill);
color: var(--text);
font-family: "JetBrains Mono", monospace;
font-size: 12px;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.wallet-pill:hover {
background: var(--surface-2);
border-color: var(--line-2);
}
.wallet-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 10px var(--accent-2);
}
/* ── Hero card ────────────────────────── */
.hero {
position: relative;
border-radius: var(--r-lg);
padding: 24px;
background:
linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(135deg, var(--accent), var(--accent-2)) border-box;
border: 1px solid transparent;
overflow: hidden;
transition: background 0.5s ease;
}
.hero-glow {
position: absolute;
top: -60%;
right: -20%;
width: 360px;
height: 360px;
background: radial-gradient(circle, var(--accent-glow), transparent 65%);
filter: blur(8px);
pointer-events: none;
transition: background 0.5s ease;
}
.hero-top {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.hero-eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--pos);
border: 1px solid rgba(38, 208, 124, 0.3);
border-radius: var(--r-pill);
padding: 3px 10px;
}
.hero-badge::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 8px var(--pos);
animation: pulse 1.8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.hero-main {
position: relative;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 22px;
}
.hero-ico {
width: 56px;
height: 56px;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 24px;
color: #0a0b0f;
background: var(--accent);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16) inset, 0 0 28px -4px var(--accent-glow);
flex-shrink: 0;
transition: background 0.5s ease, box-shadow 0.5s ease;
}
.hero-name {
margin: 0 0 6px;
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
}
.hero-meta {
margin: 0;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.hero-chip {
font-size: 12px;
color: var(--muted);
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-pill);
padding: 3px 10px;
}
.hero-stats {
position: relative;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
margin-bottom: 16px;
}
.stat {
background: var(--surface-2);
padding: 12px 13px;
display: flex;
flex-direction: column;
gap: 5px;
}
.stat-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.stat-value {
font-size: 15px;
font-weight: 500;
}
.hero-hint {
position: relative;
margin: 0;
font-size: 12px;
color: var(--muted);
}
.sim-note {
margin: 22px 0 0;
text-align: center;
font-size: 11px;
color: var(--muted);
letter-spacing: 0.02em;
}
/* ── Switching overlay ────────────────── */
.switching {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
background: rgba(10, 11, 15, 0.72);
backdrop-filter: blur(6px);
animation: fade 0.2s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.switching-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
padding: 30px 40px;
background: rgba(19, 21, 28, 0.9);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: 0 24px 60px -12px rgba(0, 0, 0, 0.7);
}
.spinner {
width: 38px;
height: 38px;
border-radius: 50%;
border: 3px solid var(--line-2);
border-top-color: var(--accent);
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.switching-text {
margin: 0;
font-weight: 600;
font-size: 15px;
}
.switching-sub {
margin: 0;
font-size: 12px;
color: var(--muted);
}
/* ── Toast ────────────────────────────── */
.toast-wrap {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
z-index: 70;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 16px;
background: rgba(27, 30, 39, 0.94);
border: 1px solid var(--line-2);
border-left: 3px solid var(--accent);
border-radius: var(--r-md);
font-size: 13px;
box-shadow: 0 14px 34px -8px rgba(0, 0, 0, 0.6);
animation: toastIn 0.25s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.toast.out {
animation: toastOut 0.25s ease forwards;
}
.toast-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
flex-shrink: 0;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
to { opacity: 0; transform: translateY(12px); }
}
/* ── Responsive ───────────────────────── */
@media (max-width: 520px) {
.shell {
padding: 20px 14px 44px;
}
.topbar {
flex-wrap: wrap;
}
.pill-net {
display: none;
}
.wallet-pill .wallet-addr {
display: none;
}
.wallet-pill {
padding: 9px;
}
.menu {
width: 100%;
right: auto;
left: 0;
}
.hero-stats {
grid-template-columns: repeat(2, 1fr);
}
.hero-name {
font-size: 22px;
}
.hero-ico {
width: 48px;
height: 48px;
font-size: 20px;
}
}(function () {
"use strict";
/* ── Mock chain data (fictional / illustrative) ── */
const CHAINS = [
{
id: "ethereum",
name: "Ethereum",
net: "Mainnet",
glyph: "E",
color: "#7c5cff",
glow: "rgba(124,92,255,0.45)",
accent2: "#9b85ff",
chainId: 1,
currency: "ETH",
gas: "14.2 gwei",
block: "21,948,113",
latency: "112 ms",
rpc: "rpc.nova-eth.xyz",
status: "online",
testnet: false,
},
{
id: "arbitrum",
name: "Arbitrum",
net: "L2 · Rollup",
glyph: "A",
color: "#2f9bff",
glow: "rgba(47,155,255,0.45)",
accent2: "#22d3ee",
chainId: 42161,
currency: "ETH",
gas: "0.06 gwei",
block: "284,110,042",
latency: "61 ms",
rpc: "rpc.arb-nova.xyz",
status: "online",
testnet: false,
},
{
id: "base",
name: "Base",
net: "L2 · OP Stack",
glyph: "B",
color: "#3b6bff",
glow: "rgba(59,107,255,0.45)",
accent2: "#4f8bff",
chainId: 8453,
currency: "ETH",
gas: "0.04 gwei",
block: "19,002,771",
latency: "58 ms",
rpc: "rpc.base-nova.xyz",
status: "online",
testnet: false,
},
{
id: "optimism",
name: "Optimism",
net: "L2 · OP Stack",
glyph: "O",
color: "#ff4d6d",
glow: "rgba(255,77,109,0.42)",
accent2: "#ff7a99",
chainId: 10,
currency: "ETH",
gas: "0.05 gwei",
block: "127,884,330",
latency: "67 ms",
rpc: "rpc.op-nova.xyz",
status: "online",
testnet: false,
},
{
id: "polygon",
name: "Polygon",
net: "PoS",
glyph: "P",
color: "#9d6bff",
glow: "rgba(157,107,255,0.45)",
accent2: "#b89bff",
chainId: 137,
currency: "POL",
gas: "31.0 gwei",
block: "65,440,219",
latency: "143 ms",
rpc: "rpc.poly-nova.xyz",
status: "degraded",
testnet: false,
},
{
id: "lumen",
name: "Lumen Chain",
net: "ZK · Mainnet",
glyph: "L",
color: "#00e0c6",
glow: "rgba(0,224,198,0.45)",
accent2: "#4dffe6",
chainId: 7777,
currency: "LUM",
gas: "0.01 gwei",
block: "4,201,556",
latency: "44 ms",
rpc: "rpc.lumenchain.xyz",
status: "online",
testnet: false,
},
{
id: "sepolia",
name: "Sepolia",
net: "Ethereum Testnet",
glyph: "S",
color: "#ffb347",
glow: "rgba(255,179,71,0.42)",
accent2: "#ffcd7a",
chainId: 11155111,
currency: "sETH",
gas: "2.10 gwei",
block: "7,310,884",
latency: "98 ms",
rpc: "rpc.sepolia-nova.xyz",
status: "online",
testnet: true,
},
{
id: "lumen-test",
name: "Lumen Testnet",
net: "ZK · Testnet",
glyph: "L",
color: "#26d07c",
glow: "rgba(38,208,124,0.42)",
accent2: "#5fe6a3",
chainId: 77770,
currency: "tLUM",
gas: "0.00 gwei",
block: "1,118,402",
latency: "39 ms",
rpc: "rpc.test.lumenchain.xyz",
status: "online",
testnet: true,
},
];
/* ── DOM refs ── */
const switcher = document.getElementById("switcher");
const pill = document.getElementById("chainPill");
const menu = document.getElementById("chainMenu");
const list = document.getElementById("chainList");
const searchInput = document.getElementById("searchInput");
const emptyState = document.getElementById("emptyState");
const testnetToggle = document.getElementById("testnetToggle");
const menuHint = document.getElementById("menuHint");
const pillIco = document.getElementById("pillIco");
const pillName = document.getElementById("pillName");
const pillNet = document.getElementById("pillNet");
const pillStatus = document.getElementById("pillStatus");
const heroIco = document.getElementById("heroIco");
const heroName = document.getElementById("heroName");
const heroChainId = document.getElementById("heroChainId");
const heroCurrency = document.getElementById("heroCurrency");
const statGas = document.getElementById("statGas");
const statBlock = document.getElementById("statBlock");
const statLatency = document.getElementById("statLatency");
const statRpc = document.getElementById("statRpc");
const switching = document.getElementById("switching");
const switchingSub = document.getElementById("switchingSub");
const walletPill = document.getElementById("walletPill");
let activeId = "ethereum";
let isOpen = false;
let busy = false;
const byId = (id) => CHAINS.find((c) => c.id === id);
/* ── Toast helper ── */
const toastWrap = document.getElementById("toastWrap");
function toast(msg, color) {
const el = document.createElement("div");
el.className = "toast";
if (color) el.style.borderLeftColor = color;
const dot = document.createElement("span");
dot.className = "toast-dot";
if (color) dot.style.background = color;
if (color) dot.style.boxShadow = "0 0 10px " + color;
const txt = document.createElement("span");
txt.innerHTML = msg;
el.appendChild(dot);
el.appendChild(txt);
toastWrap.appendChild(el);
setTimeout(() => {
el.classList.add("out");
setTimeout(() => el.remove(), 260);
}, 2600);
}
/* ── Accent theming ── */
function applyAccent(chain) {
const root = document.documentElement.style;
root.setProperty("--accent", chain.color);
root.setProperty("--accent-2", chain.accent2);
root.setProperty("--accent-glow", chain.glow);
}
/* ── Render menu list ── */
function renderList() {
const q = searchInput.value.trim().toLowerCase();
const showTest = testnetToggle.checked;
list.innerHTML = "";
let shown = 0;
CHAINS.forEach((c) => {
if (c.testnet && !showTest) return;
if (q && !(c.name.toLowerCase().includes(q) || c.net.toLowerCase().includes(q))) return;
shown++;
const li = document.createElement("li");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "chain-row" + (c.id === activeId ? " is-active" : "");
btn.setAttribute("role", "option");
btn.setAttribute("aria-selected", String(c.id === activeId));
btn.dataset.id = c.id;
const ico = document.createElement("span");
ico.className = "row-ico";
ico.style.background = c.color;
ico.style.boxShadow = "0 0 0 1px rgba(255,255,255,0.14) inset, 0 0 14px -3px " + c.glow;
ico.textContent = c.glyph;
ico.setAttribute("aria-hidden", "true");
const body = document.createElement("span");
body.className = "row-body";
const nameRow = document.createElement("span");
nameRow.className = "row-name";
nameRow.appendChild(document.createTextNode(c.name));
if (c.testnet) {
const tag = document.createElement("span");
tag.className = "row-tag";
tag.textContent = "Testnet";
nameRow.appendChild(tag);
}
const sub = document.createElement("span");
sub.className = "row-sub";
sub.textContent = c.net + " · ID " + c.chainId;
body.appendChild(nameRow);
body.appendChild(sub);
const end = document.createElement("span");
end.className = "row-end";
const st = document.createElement("span");
st.className = "row-status" + (c.status === "degraded" ? " is-degraded" : "");
st.title = c.status === "degraded" ? "Degraded" : "Online";
const check = document.createElementNS("http://www.w3.org/2000/svg", "svg");
check.setAttribute("class", "check");
check.setAttribute("viewBox", "0 0 24 24");
check.innerHTML =
'<path d="M5 12l4 4L19 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>';
end.appendChild(st);
end.appendChild(check);
btn.appendChild(ico);
btn.appendChild(body);
btn.appendChild(end);
btn.addEventListener("click", () => onSelect(c.id));
li.appendChild(btn);
list.appendChild(li);
});
emptyState.hidden = shown !== 0;
list.hidden = shown === 0;
menuHint.textContent = shown + " network" + (shown === 1 ? "" : "s");
}
/* ── Open / close ── */
function openMenu() {
if (busy) return;
isOpen = true;
switcher.classList.add("open");
pill.setAttribute("aria-expanded", "true");
menu.hidden = false;
renderList();
requestAnimationFrame(() => searchInput.focus());
}
function closeMenu() {
isOpen = false;
switcher.classList.remove("open");
pill.setAttribute("aria-expanded", "false");
menu.hidden = true;
searchInput.value = "";
}
pill.addEventListener("click", () => (isOpen ? closeMenu() : openMenu()));
document.addEventListener("click", (e) => {
if (isOpen && !switcher.contains(e.target)) closeMenu();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isOpen) {
closeMenu();
pill.focus();
}
});
searchInput.addEventListener("input", renderList);
testnetToggle.addEventListener("change", () => {
renderList();
toast(
testnetToggle.checked ? "Testnets <strong>shown</strong>" : "Testnets <strong>hidden</strong>"
);
});
/* Arrow-key navigation inside the list */
menu.addEventListener("keydown", (e) => {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
const rows = Array.from(list.querySelectorAll(".chain-row"));
if (!rows.length) return;
e.preventDefault();
const cur = rows.indexOf(document.activeElement);
let next;
if (e.key === "ArrowDown") next = cur < 0 ? 0 : Math.min(cur + 1, rows.length - 1);
else next = cur <= 0 ? rows.length - 1 : cur - 1;
rows[next].focus();
});
/* ── Select / switch ── */
function onSelect(id) {
if (id === activeId) {
closeMenu();
toast("Already on <strong>" + byId(id).name + "</strong>", byId(id).color);
return;
}
const chain = byId(id);
closeMenu();
busy = true;
pill.setAttribute("disabled", "");
switchingSub.innerHTML = 'Connecting to <span class="mono">' + chain.name + "</span>";
switching.hidden = false;
const delay = 900 + Math.random() * 700;
setTimeout(() => {
switching.hidden = true;
busy = false;
pill.removeAttribute("disabled");
activeId = id;
applyChain(chain);
toast(
'Switched to <strong>' + chain.name + "</strong>",
chain.color
);
}, delay);
}
/* ── Apply active chain to UI ── */
function applyChain(chain) {
applyAccent(chain);
// Pill
pillIco.innerHTML = '<span class="ico-glyph">' + chain.glyph + "</span>";
pillIco.style.background = chain.color;
pillIco.style.boxShadow =
"0 0 0 1px rgba(255,255,255,0.14) inset, 0 0 14px -2px " + chain.glow;
pillName.textContent = chain.name;
pillNet.textContent = chain.net;
pillStatus.className =
"status-dot " + (chain.status === "degraded" ? "is-degraded" : "is-online");
// Hero
heroIco.innerHTML = '<span class="ico-glyph">' + chain.glyph + "</span>";
heroIco.style.background = chain.color;
heroName.textContent = chain.name;
heroChainId.textContent = "Chain ID " + chain.chainId;
heroCurrency.textContent = chain.currency;
animateBlock(statBlock, chain.block);
statGas.textContent = chain.gas;
statLatency.textContent = chain.latency;
statRpc.textContent = chain.rpc;
}
/* ── Animated count-up for the block number ── */
function animateBlock(el, target) {
const clean = parseInt(target.replace(/[^0-9]/g, ""), 10);
if (!isFinite(clean)) {
el.textContent = target;
return;
}
const dur = 700;
const start = performance.now();
const fmt = (n) => n.toLocaleString("en-US");
function frame(now) {
const t = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - t, 3);
el.textContent = fmt(Math.round(clean * eased));
if (t < 1) requestAnimationFrame(frame);
else el.textContent = target;
}
requestAnimationFrame(frame);
}
walletPill.addEventListener("click", () => {
navigator.clipboard &&
navigator.clipboard.writeText("0x7a3f9e2b08d4c6515f0a1d77b3e9c41d").catch(() => {});
toast('Address copied · <span class="mono">0x7a3f…c41d</span>', "#00e0c6");
});
/* ── Init ── */
applyChain(byId(activeId));
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Network / Chain Switcher</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg-orb" aria-hidden="true"></div>
<main class="shell">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">NovaSwap</span>
<span class="brand-tag">demo</span>
</div>
<div class="topbar-right">
<!-- Network switcher -->
<div class="switcher" id="switcher">
<button
class="chain-pill"
id="chainPill"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="chainMenu"
>
<span class="chain-ico" id="pillIco" aria-hidden="true">
<span class="ico-glyph">E</span>
</span>
<span class="pill-text">
<span class="pill-name" id="pillName">Ethereum</span>
<span class="pill-net" id="pillNet">Mainnet</span>
</span>
<span class="status-dot is-online" id="pillStatus" aria-hidden="true"></span>
<svg class="chev" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div class="menu" id="chainMenu" role="dialog" aria-label="Select a network" hidden>
<div class="menu-head">
<h2 class="menu-title">Switch network</h2>
<span class="menu-hint" id="menuHint">Pick a chain to connect</span>
</div>
<div class="search">
<svg class="search-ico" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
<path d="M21 21l-4.3-4.3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<input
type="text"
id="searchInput"
class="search-input"
placeholder="Search networks"
autocomplete="off"
spellcheck="false"
aria-label="Search networks"
/>
<kbd class="search-kbd">esc</kbd>
</div>
<ul class="chain-list" id="chainList" role="listbox" aria-label="Networks"></ul>
<p class="empty" id="emptyState" hidden>No networks match that search.</p>
<div class="testnet-bar">
<label class="switch" for="testnetToggle">
<span class="switch-label">
<span class="switch-title">Show testnets</span>
<span class="switch-sub">Sepolia, Lumen Testnet & more</span>
</span>
<input type="checkbox" id="testnetToggle" class="switch-input" />
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
</label>
</div>
</div>
</div>
<button class="wallet-pill" id="walletPill" type="button">
<span class="wallet-dot" aria-hidden="true"></span>
<span class="wallet-addr">0x7a3f…c41d</span>
</button>
</div>
</header>
<section class="hero">
<div class="hero-glow" aria-hidden="true"></div>
<div class="hero-top">
<span class="hero-eyebrow">Connected network</span>
<span class="hero-badge" id="heroBadge">Live</span>
</div>
<div class="hero-main">
<span class="hero-ico" id="heroIco" aria-hidden="true"><span class="ico-glyph">E</span></span>
<div class="hero-id">
<h1 class="hero-name" id="heroName">Ethereum</h1>
<p class="hero-meta">
<span class="hero-chip" id="heroChainId">Chain ID 1</span>
<span class="hero-chip mono" id="heroCurrency">ETH</span>
</p>
</div>
</div>
<div class="hero-stats">
<div class="stat">
<span class="stat-label">Gas (base)</span>
<span class="stat-value mono" id="statGas">—</span>
</div>
<div class="stat">
<span class="stat-label">Block</span>
<span class="stat-value mono" id="statBlock">—</span>
</div>
<div class="stat">
<span class="stat-label">Latency</span>
<span class="stat-value mono" id="statLatency">—</span>
</div>
<div class="stat">
<span class="stat-label">RPC</span>
<span class="stat-value mono" id="statRpc">—</span>
</div>
</div>
<p class="hero-hint">Open the network pill in the header to switch chains. The page accent follows the active network.</p>
</section>
<p class="sim-note">UI-only simulation — no real wallet, RPC, or on-chain calls.</p>
</main>
<!-- Switching overlay -->
<div class="switching" id="switching" hidden>
<div class="switching-card">
<span class="spinner" aria-hidden="true"></span>
<p class="switching-text">Switching network…</p>
<p class="switching-sub" id="switchingSub">Connecting to <span class="mono">Ethereum</span></p>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Network / Chain Switcher
A compact chain selector built for Web3 dapp headers. The current-network pill shows a colored token icon, the chain name, its network type, and a live status dot. Clicking it opens a glassy, blurred dropdown listing six fictional networks — Ethereum, Arbitrum, Base, Optimism, Polygon and Lumen Chain — each row carrying an icon, a chain ID, and an online or degraded indicator.
A search input filters the list as you type, arrow keys move focus between rows, and esc closes the menu. A Show testnets toggle at the bottom reveals additional testnet chains (Sepolia, Lumen Testnet) tagged with a badge. Picking a network shows a brief Switching network… overlay with a simulated RPC delay, then commits the change.
On switch, everything retints: the pill, the hero card icon, the page background orbs and all accent colors follow the active chain. The hero block height counts up with an eased animation, and gas, latency and RPC values render in monospace. A small toast confirms each action, and the wallet pill copies a fictional address. Everything is vanilla JS — no wallet, no libraries.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.