Web3 — Connect Wallet Button + Modal
A polished Web3 connect-wallet flow built with vanilla JS. A glowing neon Connect Wallet button opens a glassy modal listing MetaMask, Coinbase Wallet, WalletConnect, Rabby and Ledger, each with an icon plus an Installed, Popular or Hardware badge. Picking one shows a simulated connecting spinner, then swaps the button for a live address chip with a NOVA balance, green status dot and a dropdown to copy the address, open the explorer or disconnect.
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;
}
*,
*::before,
*::after {
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;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-variant-numeric: tabular-nums;
}
button {
font-family: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---- ambient background ---- */
.aurora {
position: fixed;
inset: -20% -10% auto -10%;
height: 70vh;
pointer-events: none;
z-index: 0;
background:
radial-gradient(40% 60% at 20% 10%, rgba(124, 92, 255, 0.28), transparent 70%),
radial-gradient(45% 55% at 85% 0%, rgba(0, 224, 198, 0.22), transparent 70%);
filter: blur(20px);
}
.stage {
position: relative;
z-index: 1;
max-width: 980px;
margin: 0 auto;
padding: 28px 24px 80px;
}
/* ---- masthead ---- */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
backdrop-filter: blur(12px);
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
}
.brand-mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: var(--r-sm);
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #0a0b0f;
box-shadow: 0 0 18px var(--accent-glow);
font-size: 15px;
}
.brand-name {
font-size: 17px;
}
.brand-chain {
font-size: 12px;
color: var(--muted);
padding: 3px 9px;
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
/* ---- connect button ---- */
.btn-connect {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 18px;
font-size: 14.5px;
font-weight: 600;
color: #0a0b0f;
border: none;
border-radius: var(--r-pill);
background: linear-gradient(135deg, var(--accent), #9d7bff 60%, var(--accent-2));
box-shadow: 0 6px 22px var(--accent-glow), inset 0 0 0 1px rgba(255, 255, 255, 0.25);
transition: transform 0.16s ease, box-shadow 0.16s ease, filter 0.16s ease;
}
.btn-connect:hover {
transform: translateY(-1px);
box-shadow: 0 10px 30px var(--accent-glow), inset 0 0 0 1px rgba(255, 255, 255, 0.35);
filter: brightness(1.05);
}
.btn-connect:active {
transform: translateY(0);
}
.btn-connect .bolt {
font-size: 13px;
}
.btn-ghost {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 18px;
font-size: 14px;
font-weight: 500;
color: var(--text);
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--line-2);
border-radius: var(--r-pill);
transition: border-color 0.16s ease, background 0.16s ease;
}
.btn-ghost:hover {
border-color: var(--accent);
background: rgba(124, 92, 255, 0.1);
}
.btn-ghost.sm {
padding: 8px 16px;
font-size: 13px;
}
/* ---- connected chip + dropdown ---- */
.account {
position: relative;
}
.chip {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 7px 8px 7px 12px;
border-radius: var(--r-pill);
border: 1px solid var(--line-2);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(10px);
color: var(--text);
transition: border-color 0.16s ease, background 0.16s ease;
animation: chipIn 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.chip:hover {
border-color: var(--accent);
background: rgba(124, 92, 255, 0.1);
}
@keyframes chipIn {
from {
opacity: 0;
transform: translateY(-6px) scale(0.96);
}
}
.chip-bal {
font-size: 13px;
font-weight: 500;
color: var(--accent-2);
}
.chip-sep {
width: 1px;
height: 18px;
background: var(--line-2);
}
.chip-addr {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 13px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.dot-live {
background: var(--pos);
box-shadow: 0 0 0 0 rgba(38, 208, 124, 0.6);
animation: pulse 2s infinite;
}
@keyframes pulse {
70% {
box-shadow: 0 0 0 7px rgba(38, 208, 124, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(38, 208, 124, 0);
}
}
.chip-caret {
font-size: 10px;
color: var(--muted);
margin-left: 2px;
}
.dropdown {
position: absolute;
top: calc(100% + 10px);
right: 0;
width: 268px;
padding: 8px;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--surface-2);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
z-index: 30;
animation: ddIn 0.18s ease;
}
@keyframes ddIn {
from {
opacity: 0;
transform: translateY(-6px);
}
}
.dd-head {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px 12px;
border-bottom: 1px solid var(--line);
margin-bottom: 6px;
}
.dd-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: conic-gradient(from 180deg, var(--accent), var(--accent-2), var(--accent));
flex: none;
}
.dd-id .dd-addr {
font-size: 13px;
}
.dd-id .dd-bal {
font-size: 12px;
color: var(--muted);
}
.dd-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 9px 12px;
border: none;
background: transparent;
color: var(--text);
font-size: 13.5px;
text-align: left;
border-radius: var(--r-sm);
transition: background 0.14s ease;
}
.dd-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.dd-item.danger {
color: var(--neg);
}
.dd-item.danger:hover {
background: rgba(255, 77, 109, 0.12);
}
.dd-item .ic {
width: 18px;
text-align: center;
opacity: 0.85;
}
/* ---- hero ---- */
.hero {
margin-top: 44px;
max-width: 640px;
}
.eyebrow {
display: inline-block;
margin: 0 0 14px;
font-size: 11.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-2);
padding: 4px 11px;
border: 1px solid rgba(0, 224, 198, 0.3);
border-radius: var(--r-pill);
background: rgba(0, 224, 198, 0.06);
}
.hero h1 {
margin: 0 0 14px;
font-size: clamp(28px, 5vw, 42px);
line-height: 1.12;
font-weight: 700;
letter-spacing: -0.02em;
}
.grad {
background: linear-gradient(120deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede {
margin: 0 0 22px;
color: var(--muted);
font-size: 15.5px;
}
.hero-actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.net-pill {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
}
/* ---- meta cards ---- */
.meta-cards {
margin-top: 40px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.meta-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 16px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.01));
backdrop-filter: blur(8px);
position: relative;
overflow: hidden;
}
.meta-card::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), transparent);
opacity: 0.6;
}
.meta-label {
font-size: 12px;
color: var(--muted);
}
.meta-value {
font-size: 21px;
font-weight: 500;
}
.meta-unit {
font-size: 12px;
color: var(--muted);
}
.chg {
font-size: 12px;
font-weight: 600;
}
.chg.pos {
color: var(--pos);
}
.chg.neg {
color: var(--neg);
}
/* ---- overlay + modal ---- */
.overlay {
position: fixed;
inset: 0;
z-index: 40;
display: grid;
place-items: center;
padding: 20px;
background: rgba(6, 7, 11, 0.66);
backdrop-filter: blur(6px);
animation: fade 0.2s ease;
}
.overlay[hidden] {
display: none;
}
@keyframes fade {
from {
opacity: 0;
}
}
.modal {
width: min(420px, 100%);
border-radius: var(--r-lg);
background: linear-gradient(180deg, rgba(27, 30, 39, 0.95), rgba(19, 21, 28, 0.95));
backdrop-filter: blur(18px);
position: relative;
padding: 20px 20px 0;
animation: pop 0.26s cubic-bezier(0.2, 0.9, 0.3, 1.2);
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55);
}
/* gradient border */
.modal::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(140deg, var(--accent), transparent 40%, var(--accent-2));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
@keyframes pop {
from {
opacity: 0;
transform: translateY(10px) scale(0.97);
}
}
.modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.modal-head h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.modal-sub {
margin: 3px 0 0;
font-size: 13px;
color: var(--muted);
}
.icon-btn {
flex: none;
width: 32px;
height: 32px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
font-size: 14px;
transition: color 0.14s, border-color 0.14s, background 0.14s;
}
.icon-btn:hover {
color: var(--text);
border-color: var(--line-2);
background: rgba(255, 255, 255, 0.07);
}
/* wallet list */
.wallet-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.wallet-row {
display: flex;
align-items: center;
gap: 13px;
width: 100%;
padding: 12px 14px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.025);
color: var(--text);
text-align: left;
transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
}
.wallet-row:hover {
border-color: var(--accent);
background: rgba(124, 92, 255, 0.08);
transform: translateX(2px);
}
.w-icon {
flex: none;
width: 38px;
height: 38px;
display: grid;
place-items: center;
font-size: 19px;
border-radius: 11px;
background: var(--elevated);
border: 1px solid var(--line);
}
.w-meta {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.w-name {
font-size: 14.5px;
font-weight: 600;
}
.w-desc {
font-size: 12px;
color: var(--muted);
}
.badge {
flex: none;
font-size: 11px;
font-weight: 600;
padding: 3px 9px;
border-radius: var(--r-pill);
}
.badge-installed {
color: var(--pos);
background: rgba(38, 208, 124, 0.12);
border: 1px solid rgba(38, 208, 124, 0.3);
}
.badge-popular {
color: var(--accent-2);
background: rgba(0, 224, 198, 0.1);
border: 1px solid rgba(0, 224, 198, 0.28);
}
.badge-hw {
color: var(--warn);
background: rgba(255, 179, 71, 0.12);
border: 1px solid rgba(255, 179, 71, 0.3);
}
/* connecting state */
.connecting {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
padding: 26px 10px 10px;
}
.connecting[hidden] {
display: none;
}
.spinner {
width: 54px;
height: 54px;
border-radius: 50%;
margin-bottom: 6px;
background: conic-gradient(from 0deg, transparent, var(--accent), var(--accent-2));
-webkit-mask: radial-gradient(circle 22px at center, transparent 98%, #000);
mask: radial-gradient(circle 22px at center, transparent 98%, #000);
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.connecting-title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.connecting-title span {
color: var(--accent-2);
}
.connecting-sub {
margin: 0 0 6px;
font-size: 13px;
color: var(--muted);
}
.modal-foot {
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
padding: 14px 4px;
border-top: 1px solid var(--line);
}
.modal-foot p {
margin: 0;
font-size: 11.5px;
color: var(--muted);
}
.shield {
font-size: 15px;
}
/* ---- toast ---- */
.toast-wrap {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
z-index: 60;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 16px;
border-radius: var(--r-pill);
background: var(--elevated);
border: 1px solid var(--line-2);
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
font-size: 13.5px;
animation: toastIn 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
.toast.out {
animation: toastOut 0.3s ease forwards;
}
.toast .t-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-2);
}
.toast.ok .t-dot {
background: var(--pos);
}
.toast.warn .t-dot {
background: var(--warn);
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateY(12px);
}
}
@keyframes toastOut {
to {
opacity: 0;
transform: translateY(12px);
}
}
/* ---- responsive ---- */
@media (max-width: 720px) {
.meta-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.stage {
padding: 18px 16px 64px;
}
.masthead {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.wallet-zone {
display: flex;
justify-content: stretch;
}
.btn-connect,
.account {
width: 100%;
}
.btn-connect {
justify-content: center;
}
.chip {
width: 100%;
justify-content: space-between;
}
.dropdown {
width: 100%;
}
.hero h1 {
font-size: 27px;
}
.meta-value {
font-size: 18px;
}
.modal {
padding: 16px 16px 0;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
// ---- mock state (UI simulation only — no real wallet/RPC) ----
var ADDRESS = "0x7a3f9e2bC44102d8aBcD0099f1ee77b321aFc41d";
var SHORT = "0x7a3f…c41d";
var state = { connected: false, wallet: null, balance: 0 };
var connectTimer = null;
// ---- elements ----
var walletZone = document.getElementById("walletZone");
var connectBtn = document.getElementById("connectBtn");
var heroConnect = document.getElementById("heroConnect");
var overlay = document.getElementById("overlay");
var modal = document.getElementById("modal");
var modalClose = document.getElementById("modalClose");
var walletList = document.getElementById("walletList");
var connecting = document.getElementById("connecting");
var connectingName = document.getElementById("connectingName");
var connectingCancel = document.getElementById("connectingCancel");
var toastWrap = document.getElementById("toastWrap");
var lastFocused = null;
// ---- toast helper ----
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " " + kind : "");
el.innerHTML = '<span class="t-dot"></span><span></span>';
el.querySelector("span:last-child").textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
setTimeout(function () {
el.remove();
}, 300);
}, 2600);
}
// ---- focus trap ----
function focusable() {
return Array.prototype.filter.call(
modal.querySelectorAll(
'button, [href], input, [tabindex]:not([tabindex="-1"])'
),
function (el) {
return el.offsetParent !== null && !el.disabled;
}
);
}
function trap(e) {
if (e.key !== "Tab") return;
var items = focusable();
if (!items.length) return;
var first = items[0];
var last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
// ---- modal open / close ----
function openModal() {
lastFocused = document.activeElement;
overlay.hidden = false;
resetModal();
document.addEventListener("keydown", onKeydown);
var first = walletList.querySelector(".wallet-row");
if (first) first.focus();
}
function closeModal() {
overlay.hidden = true;
if (connectTimer) {
clearTimeout(connectTimer);
connectTimer = null;
}
document.removeEventListener("keydown", onKeydown);
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
function resetModal() {
walletList.hidden = false;
connecting.hidden = true;
}
function onKeydown(e) {
if (e.key === "Escape") {
e.preventDefault();
closeModal();
} else {
trap(e);
}
}
// ---- connect simulation ----
function startConnect(name, balance) {
connectingName.textContent = name;
walletList.hidden = true;
connecting.hidden = false;
connectingCancel.focus();
connectTimer = setTimeout(function () {
connectTimer = null;
state.connected = true;
state.wallet = name;
state.balance = balance;
renderConnected();
closeModal();
toast("Connected to " + name, "ok");
}, 1700);
}
function cancelConnect() {
if (connectTimer) {
clearTimeout(connectTimer);
connectTimer = null;
}
resetModal();
var first = walletList.querySelector(".wallet-row");
if (first) first.focus();
toast("Connection cancelled", "warn");
}
// ---- connected chip + dropdown ----
function renderConnected() {
walletZone.innerHTML = "";
var account = document.createElement("div");
account.className = "account";
var chip = document.createElement("button");
chip.type = "button";
chip.className = "chip";
chip.setAttribute("aria-haspopup", "menu");
chip.setAttribute("aria-expanded", "false");
chip.innerHTML =
'<span class="chip-bal mono">' +
state.balance.toFixed(3) +
" NOVA</span>" +
'<span class="chip-sep"></span>' +
'<span class="chip-addr"><span class="dot dot-live"></span><span class="mono">' +
SHORT +
"</span></span>" +
'<span class="chip-caret">▾</span>';
var dd = document.createElement("div");
dd.className = "dropdown";
dd.setAttribute("role", "menu");
dd.hidden = true;
dd.innerHTML =
'<div class="dd-head">' +
'<span class="dd-avatar"></span>' +
'<div class="dd-id"><div class="dd-addr mono">' +
SHORT +
'</div><div class="dd-bal mono">' +
state.balance.toFixed(3) +
" NOVA · " +
state.wallet +
"</div></div>" +
"</div>" +
'<button class="dd-item" role="menuitem" data-act="copy"><span class="ic">⧉</span>Copy address</button>' +
'<button class="dd-item" role="menuitem" data-act="explorer"><span class="ic">↗</span>View on Lumen Scan</button>' +
'<button class="dd-item danger" role="menuitem" data-act="disconnect"><span class="ic">⏻</span>Disconnect</button>';
account.appendChild(chip);
account.appendChild(dd);
walletZone.appendChild(account);
function setOpen(open) {
dd.hidden = !open;
chip.setAttribute("aria-expanded", open ? "true" : "false");
if (open) {
document.addEventListener("click", onOutside, true);
document.addEventListener("keydown", onDdKey);
var f = dd.querySelector(".dd-item");
if (f) f.focus();
} else {
document.removeEventListener("click", onOutside, true);
document.removeEventListener("keydown", onDdKey);
}
}
function onOutside(e) {
if (!account.contains(e.target)) setOpen(false);
}
function onDdKey(e) {
if (e.key === "Escape") {
setOpen(false);
chip.focus();
}
}
chip.addEventListener("click", function () {
setOpen(dd.hidden);
});
dd.addEventListener("click", function (e) {
var item = e.target.closest(".dd-item");
if (!item) return;
var act = item.getAttribute("data-act");
if (act === "copy") {
copyAddress();
} else if (act === "explorer") {
toast("Opening Lumen Scan (simulated)");
} else if (act === "disconnect") {
disconnect();
}
setOpen(false);
});
}
function copyAddress() {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(ADDRESS).then(
function () {
toast("Address copied", "ok");
},
function () {
toast("Copy failed", "warn");
}
);
} else {
toast("Address copied", "ok");
}
}
function disconnect() {
state.connected = false;
state.wallet = null;
state.balance = 0;
walletZone.innerHTML = "";
var btn = document.createElement("button");
btn.className = "btn-connect";
btn.id = "connectBtn";
btn.type = "button";
btn.setAttribute("aria-haspopup", "dialog");
btn.innerHTML =
'<span class="bolt" aria-hidden="true">⚡</span><span>Connect Wallet</span>';
walletZone.appendChild(btn);
btn.addEventListener("click", openModal);
btn.focus();
toast("Wallet disconnected", "warn");
}
// ---- animated numbers ----
function animateCounts() {
var nodes = document.querySelectorAll("[data-count]");
nodes.forEach(function (node) {
var target = parseFloat(node.getAttribute("data-count"));
var dec = parseInt(node.getAttribute("data-dec") || "0", 10);
var start = performance.now();
var dur = 1100;
function step(now) {
var p = Math.min((now - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
var val = target * eased;
node.textContent =
dec > 0
? val.toFixed(dec)
: Math.round(val).toLocaleString("en-US");
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
}
// ---- wire up ----
connectBtn.addEventListener("click", openModal);
heroConnect.addEventListener("click", openModal);
modalClose.addEventListener("click", closeModal);
connectingCancel.addEventListener("click", cancelConnect);
overlay.addEventListener("click", function (e) {
if (e.target === overlay) closeModal();
});
walletList.addEventListener("click", function (e) {
var row = e.target.closest(".wallet-row");
if (!row) return;
startConnect(row.getAttribute("data-wallet"), parseFloat(row.getAttribute("data-balance")));
});
animateCounts();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web3 — Connect Wallet</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="aurora" aria-hidden="true"></div>
<main class="stage">
<header class="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◈</span>
<span class="brand-name">NovaSwap</span>
<span class="brand-chain">Lumen Chain</span>
</div>
<!-- This wrapper swaps between the connect button and the connected chip -->
<div class="wallet-zone" id="walletZone">
<button class="btn-connect" id="connectBtn" type="button" aria-haspopup="dialog">
<span class="bolt" aria-hidden="true">⚡</span>
<span>Connect Wallet</span>
</button>
</div>
</header>
<section class="hero">
<p class="eyebrow">UI-only simulation</p>
<h1>Connect a wallet to start trading on <span class="grad">NovaSwap</span></h1>
<p class="lede">
A polished, self-contained Connect Wallet flow — modal with wallet options, a simulated
connecting state, and a live address chip with balance. No real wallet or RPC is ever
touched.
</p>
<div class="hero-actions">
<button class="btn-ghost" id="heroConnect" type="button">Launch wallet picker</button>
<span class="net-pill"><i class="dot dot-live"></i> Lumen Mainnet</span>
</div>
</section>
<section class="meta-cards" aria-label="Network stats">
<article class="meta-card">
<span class="meta-label">Gas (fast)</span>
<span class="meta-value mono" data-count="12">0</span><span class="meta-unit"> gwei</span>
</article>
<article class="meta-card">
<span class="meta-label">NOVA price</span>
<span class="meta-value mono">$<span data-count="4.82" data-dec="2">0</span></span>
<span class="chg pos">+3.1%</span>
</article>
<article class="meta-card">
<span class="meta-label">Block</span>
<span class="meta-value mono" data-count="19284517">0</span>
</article>
<article class="meta-card">
<span class="meta-label">TVL</span>
<span class="meta-value mono">$<span data-count="48.6" data-dec="1">0</span>M</span>
</article>
</section>
</main>
<!-- ===== Connect modal ===== -->
<div class="overlay" id="overlay" hidden>
<div
class="modal"
id="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modalTitle"
aria-describedby="modalDesc"
>
<header class="modal-head">
<div>
<h2 id="modalTitle">Connect a wallet</h2>
<p id="modalDesc" class="modal-sub">Choose how you'd like to connect</p>
</div>
<button class="icon-btn" id="modalClose" type="button" aria-label="Close dialog">✕</button>
</header>
<ul class="wallet-list" id="walletList" role="list">
<li>
<button class="wallet-row" type="button" data-wallet="MetaMask" data-balance="2.418">
<span class="w-icon ic-mm" aria-hidden="true">🦊</span>
<span class="w-meta">
<span class="w-name">MetaMask</span>
<span class="w-desc mono">0x7a3f…c41d</span>
</span>
<span class="badge badge-installed">Installed</span>
</button>
</li>
<li>
<button class="wallet-row" type="button" data-wallet="Coinbase Wallet" data-balance="0.812">
<span class="w-icon ic-cb" aria-hidden="true">🔵</span>
<span class="w-meta">
<span class="w-name">Coinbase Wallet</span>
<span class="w-desc">Smart wallet + passkeys</span>
</span>
<span class="badge badge-popular">Popular</span>
</button>
</li>
<li>
<button class="wallet-row" type="button" data-wallet="WalletConnect" data-balance="5.004">
<span class="w-icon ic-wc" aria-hidden="true">🔗</span>
<span class="w-meta">
<span class="w-name">WalletConnect</span>
<span class="w-desc">Scan with 400+ mobile wallets</span>
</span>
<span class="badge badge-popular">Popular</span>
</button>
</li>
<li>
<button class="wallet-row" type="button" data-wallet="Rabby" data-balance="1.337">
<span class="w-icon ic-rb" aria-hidden="true">🐰</span>
<span class="w-meta">
<span class="w-name">Rabby</span>
<span class="w-desc mono">0x9c11…a0f2</span>
</span>
<span class="badge badge-installed">Installed</span>
</button>
</li>
<li>
<button class="wallet-row" type="button" data-wallet="Ledger" data-balance="9.901">
<span class="w-icon ic-lg" aria-hidden="true">▰</span>
<span class="w-meta">
<span class="w-name">Ledger</span>
<span class="w-desc">Hardware — confirm on device</span>
</span>
<span class="badge badge-hw">Hardware</span>
</button>
</li>
</ul>
<!-- Connecting state (hidden by default) -->
<div class="connecting" id="connecting" hidden>
<div class="spinner" aria-hidden="true"></div>
<p class="connecting-title">Connecting to <span id="connectingName">MetaMask</span>…</p>
<p class="connecting-sub">Approve the connection request in your wallet</p>
<button class="btn-ghost sm" id="connectingCancel" type="button">Cancel</button>
</div>
<footer class="modal-foot">
<span class="shield" aria-hidden="true">🛡️</span>
<p>NovaSwap never has access to your funds. This is a UI simulation — nothing is signed.</p>
</footer>
</div>
</div>
<!-- ===== Connected account dropdown (template injected via JS) ===== -->
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Connect Wallet Button + Modal
A complete, self-contained Connect Wallet experience for a fictional NovaSwap dApp on the
Lumen Chain. The primary action is a gradient, glow-shadowed button that opens a glassy modal
(gradient border, backdrop blur) listing five wallet options — MetaMask, Coinbase Wallet,
WalletConnect, Rabby and Ledger — each with an icon, supporting copy, and an Installed,
Popular or Hardware badge.
Selecting a wallet transitions the modal into a connecting state with an animated conic spinner,
then resolves into the connected view: the button is replaced by an address chip showing a
truncated monospace address (0x7a3f…c41d), the wallet’s NOVA balance, and a pulsing green
status dot. The chip opens a dropdown with an account avatar, copy-address, view-on-explorer
and a clearly-styled red disconnect action that restores the original button.
Interactions are handled in dependency-free vanilla JS: overlay and Esc-to-close, a Tab focus
trap inside the dialog, a cancellable simulated connect delay, clipboard copy, a small toast()
helper, and count-up animations on the network stat cards. Monospace is used for every address,
amount and gas value, and the whole layout reflows cleanly down to ~360px.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.