Web3 — Gas / Network Fee Selector
A glassy, dark-mode gas and network-fee selector for an EIP-1559 style chain, built as a pure UI simulation. Three speed presets — Slow, Normal, Fast — render as selectable cards showing live gwei, estimated confirm time and fiat cost, while a Custom tab exposes max base fee and priority fee inputs with a computed total. A current base-fee ticker drifts every couple of seconds, recomputing every preset, the running summary and the signed-fee figure with smoothly animated monospace numbers.
MCP
Código
: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,
body {
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
background:
radial-gradient(900px 500px at 12% -10%, rgba(124, 92, 255, 0.18), transparent 60%),
radial-gradient(700px 500px at 110% 110%, rgba(0, 224, 198, 0.12), transparent 55%),
var(--bg);
color: var(--text);
font-family: "Space Grotesk", system-ui, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: grid;
place-items: center;
padding: 28px 16px;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-variant-numeric: tabular-nums;
}
.shell {
width: 100%;
max-width: 440px;
}
/* ---------- Card ---------- */
.card {
position: relative;
border-radius: var(--r-lg);
padding: 22px;
background: linear-gradient(180deg, rgba(27, 30, 39, 0.92), rgba(19, 21, 28, 0.92));
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
box-shadow:
0 30px 80px -30px rgba(0, 0, 0, 0.8),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.card::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(140deg, rgba(124, 92, 255, 0.55), rgba(0, 224, 198, 0.28) 45%, rgba(255, 255, 255, 0.04) 70%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* ---------- Header ---------- */
.card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.head__id {
display: flex;
align-items: center;
gap: 11px;
}
.net-dot {
width: 30px;
height: 30px;
border-radius: var(--r-pill);
background: conic-gradient(from 200deg, var(--accent), var(--accent-2), var(--accent));
box-shadow: 0 0 0 4px rgba(124, 92, 255, 0.12), 0 0 18px var(--accent-glow);
flex: none;
}
.card__head h1 {
margin: 0;
font-size: 19px;
font-weight: 600;
letter-spacing: -0.01em;
}
.head__sub {
margin: 1px 0 0;
font-size: 12.5px;
color: var(--muted);
}
.ticker {
display: flex;
align-items: baseline;
gap: 5px;
padding: 7px 11px;
border-radius: var(--r-md);
background: rgba(0, 0, 0, 0.28);
border: 1px solid var(--line);
}
.ticker__label {
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin-right: 2px;
}
.ticker__val {
font-size: 16px;
font-weight: 700;
transition: color 0.3s;
}
.ticker__unit {
font-size: 11px;
color: var(--muted);
}
.ticker__trend {
font-size: 10px;
transition: color 0.3s, transform 0.3s;
}
.ticker.is-up .ticker__val,
.ticker.is-up .ticker__trend {
color: var(--neg);
}
.ticker.is-down .ticker__val,
.ticker.is-down .ticker__trend {
color: var(--pos);
}
.ticker.is-down .ticker__trend {
transform: rotate(180deg);
}
/* ---------- Mode switch ---------- */
.modes {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
padding: 4px;
border-radius: var(--r-pill);
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--line);
margin-bottom: 16px;
}
.mode {
position: relative;
z-index: 1;
appearance: none;
border: 0;
background: transparent;
color: var(--muted);
font: inherit;
font-weight: 600;
font-size: 14px;
padding: 9px 12px;
border-radius: var(--r-pill);
cursor: pointer;
transition: color 0.2s;
}
.mode.is-active {
color: var(--text);
}
.modes__glow {
position: absolute;
z-index: 0;
top: 4px;
bottom: 4px;
left: 4px;
width: calc(50% - 6px);
border-radius: var(--r-pill);
background: linear-gradient(120deg, var(--accent), rgba(124, 92, 255, 0.65));
box-shadow: 0 6px 18px -6px var(--accent-glow);
transition: transform 0.28s cubic-bezier(0.3, 1.4, 0.4, 1);
}
.modes[data-active="custom"] .modes__glow {
transform: translateX(calc(100% + 4px));
}
/* ---------- Panels ---------- */
.panel.is-hidden {
display: none;
}
/* ---------- Presets ---------- */
.presets {
display: flex;
flex-direction: column;
gap: 9px;
}
.preset {
position: relative;
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"top fiat"
"gwei fiat";
align-items: center;
column-gap: 12px;
text-align: left;
appearance: none;
cursor: pointer;
padding: 13px 15px;
border-radius: var(--r-md);
background: var(--surface);
border: 1px solid var(--line);
color: var(--text);
font: inherit;
transition: border-color 0.18s, background 0.18s, transform 0.12s, box-shadow 0.18s;
}
.preset:hover {
border-color: var(--line-2);
background: var(--surface-2);
}
.preset:active {
transform: scale(0.99);
}
.preset.is-active {
border-color: rgba(124, 92, 255, 0.7);
background:
linear-gradient(180deg, rgba(124, 92, 255, 0.16), rgba(124, 92, 255, 0.04)),
var(--surface);
box-shadow: 0 0 0 1px rgba(124, 92, 255, 0.45), 0 12px 30px -16px var(--accent-glow);
}
.preset__top {
grid-area: top;
display: flex;
align-items: baseline;
gap: 9px;
}
.preset__name {
font-weight: 600;
font-size: 15px;
}
.preset__time {
font-size: 12px;
color: var(--muted);
}
.preset__gwei {
grid-area: gwei;
font-size: 13px;
font-weight: 500;
color: var(--accent-2);
margin-top: 2px;
}
.preset__gweiunit {
display: none;
}
.preset__fiat {
grid-area: fiat;
font-size: 16px;
font-weight: 700;
justify-self: end;
}
.preset__badge {
position: absolute;
top: -8px;
right: 14px;
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 3px 8px;
border-radius: var(--r-pill);
color: #0a0b0f;
background: linear-gradient(120deg, var(--accent-2), #6ef0dd);
box-shadow: 0 4px 14px -4px rgba(0, 224, 198, 0.6);
}
.preset__check {
position: absolute;
left: -1px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 0;
border-radius: 0 3px 3px 0;
background: linear-gradient(var(--accent), var(--accent-2));
transition: height 0.2s;
}
.preset.is-active .preset__check {
height: 60%;
}
/* ---------- Custom ---------- */
.custom {
display: flex;
flex-direction: column;
gap: 16px;
}
.field {
display: block;
}
.field__label {
display: flex;
align-items: center;
gap: 7px;
font-size: 13px;
font-weight: 500;
color: var(--text);
margin-bottom: 7px;
}
.field__hint {
font-size: 11px;
color: var(--muted);
font-weight: 400;
}
.field__wrap {
display: flex;
align-items: stretch;
gap: 8px;
}
.field__input {
flex: 1;
min-width: 0;
appearance: none;
background: rgba(0, 0, 0, 0.28);
border: 1px solid var(--line);
border-radius: var(--r-md);
color: var(--text);
font-size: 16px;
font-weight: 500;
padding: 11px 13px;
transition: border-color 0.18s, box-shadow 0.18s;
}
.field__input:focus {
outline: none;
border-color: rgba(124, 92, 255, 0.6);
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.2);
}
.field__sync {
appearance: none;
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--accent-2);
font: inherit;
font-weight: 600;
font-size: 13px;
padding: 0 14px;
border-radius: var(--r-md);
cursor: pointer;
transition: border-color 0.18s, background 0.18s;
}
.field__sync:hover {
border-color: var(--line-2);
background: var(--elevated);
}
.field__note {
display: block;
margin-top: 7px;
font-size: 11.5px;
color: var(--muted);
}
.prio-presets {
display: flex;
gap: 7px;
margin-top: 9px;
}
.chip {
appearance: none;
border: 1px solid var(--line);
background: rgba(0, 0, 0, 0.22);
color: var(--muted);
font: inherit;
font-size: 12px;
font-weight: 500;
padding: 6px 11px;
border-radius: var(--r-pill);
cursor: pointer;
transition: border-color 0.18s, color 0.18s, background 0.18s;
}
.chip:hover {
color: var(--text);
border-color: var(--line-2);
}
.chip.is-active {
color: var(--accent-2);
border-color: rgba(0, 224, 198, 0.5);
background: rgba(0, 224, 198, 0.08);
}
.custom__warn {
display: flex;
align-items: flex-start;
gap: 9px;
margin: 0;
padding: 11px 13px;
border-radius: var(--r-md);
background: rgba(255, 179, 71, 0.1);
border: 1px solid rgba(255, 179, 71, 0.32);
color: #ffd599;
font-size: 12.5px;
}
.warn__icon {
flex: none;
width: 18px;
height: 18px;
border-radius: var(--r-pill);
display: grid;
place-items: center;
font-weight: 700;
font-size: 12px;
color: #0a0b0f;
background: var(--warn);
}
/* ---------- Summary ---------- */
.summary {
margin-top: 18px;
padding: 14px 16px;
border-radius: var(--r-md);
background: rgba(0, 0, 0, 0.26);
border: 1px solid var(--line);
}
.summary__row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
}
.summary__k {
color: var(--muted);
}
.summary__v {
font-size: 13px;
font-weight: 500;
}
.summary__row--total {
margin-top: 6px;
padding-top: 13px;
border-top: 1px dashed var(--line-2);
}
.summary__row--total .summary__k {
color: var(--text);
font-weight: 600;
font-size: 14px;
}
.summary__total {
display: flex;
align-items: baseline;
gap: 6px;
}
.summary__eth {
font-size: 18px;
font-weight: 700;
}
.summary__sym {
font-size: 12px;
color: var(--accent-2);
font-weight: 600;
}
.summary__fiat {
font-size: 12px;
color: var(--muted);
margin-left: 4px;
}
/* ---------- Footer ---------- */
.card__foot {
margin-top: 18px;
}
.foot__meta {
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
font-size: 12px;
color: var(--muted);
margin-bottom: 12px;
}
.foot__from,
.foot__to {
color: var(--text);
}
.foot__arrow {
color: var(--accent);
}
.confirm {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
appearance: none;
border: 0;
cursor: pointer;
padding: 15px 18px;
border-radius: var(--r-md);
color: #fff;
font: inherit;
font-weight: 600;
font-size: 15px;
background: linear-gradient(120deg, var(--accent), #9a7dff 60%, var(--accent-2));
box-shadow: 0 14px 34px -12px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.3);
transition: transform 0.12s, box-shadow 0.2s, filter 0.2s;
}
.confirm:hover {
filter: brightness(1.06);
box-shadow: 0 18px 40px -12px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.confirm:active {
transform: translateY(1px) scale(0.995);
}
.confirm.is-signing {
filter: saturate(0.8) brightness(0.9);
pointer-events: none;
}
.confirm__fee {
font-weight: 700;
padding: 4px 10px;
border-radius: var(--r-pill);
background: rgba(0, 0, 0, 0.28);
font-size: 13px;
}
/* ---------- Focus ---------- */
.mode:focus-visible,
.preset:focus-visible,
.chip:focus-visible,
.field__sync:focus-visible,
.confirm:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
max-width: 88vw;
padding: 11px 16px;
border-radius: var(--r-pill);
background: rgba(27, 30, 39, 0.96);
border: 1px solid var(--line-2);
color: var(--text);
font-size: 13px;
font-weight: 500;
box-shadow: 0 18px 40px -16px rgba(0, 0, 0, 0.8);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 16px 12px;
}
.card {
padding: 18px;
}
.card__head {
flex-direction: column;
align-items: stretch;
}
.ticker {
align-self: flex-start;
}
.confirm {
padding: 14px 16px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}(() => {
"use strict";
// ---- Fictional market constants ----
const GAS_LIMIT = 21000; // standard transfer
const LUM_PRICE_USD = 3.42; // 1 LUM ≈ $3.42 (fictional)
const GWEI_TO_LUM = 1e-9; // 1 gwei = 1e-9 LUM
// Priority tips added on top of the live base fee, per preset.
const PRESETS = {
slow: { tip: 0.4, time: "~3 min", label: "Slow" },
normal: { tip: 1.5, time: "~45 s", label: "Normal" },
fast: { tip: 3.2, time: "~15 s", label: "Fast" },
};
let baseFee = 21.4; // live base fee in gwei (drifts)
let mode = "presets"; // "presets" | "custom"
let selectedPreset = "normal";
// ---- Elements ----
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const baseFeeEl = $("#baseFee");
const baseTrendEl = $("#baseTrend");
const tickerEl = $(".ticker");
const baseHintEl = $("#baseHint");
const modesEl = $(".modes");
const modeBtns = $$(".mode");
const panelPresets = $("#panel-presets");
const panelCustom = $("#panel-custom");
const presetBtns = $$(".preset");
const maxBaseEl = $("#maxBase");
const priorityEl = $("#priority");
const syncBaseBtn = $("#syncBase");
const chips = $$(".chip");
const customWarn = $("#customWarn");
const customWarnText = $("#customWarnText");
const sumGwei = $("#sumGwei");
const sumTime = $("#sumTime");
const sumEth = $("#sumEth");
const sumFiat = $("#sumFiat");
const confirmFee = $("#confirmFee");
const confirmBtn = $("#confirmBtn");
const confirmLabel = $(".confirm__label");
// ---- Helpers ----
const fmtUsd = (n) =>
"$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const toast = (() => {
const el = $("#toast");
let timer;
return (msg) => {
el.textContent = msg;
el.classList.add("is-show");
clearTimeout(timer);
timer = setTimeout(() => el.classList.remove("is-show"), 2400);
};
})();
// Animate a number element from its current value to `to`.
function animateNum(el, to, decimals, suffix = "") {
const from = parseFloat((el.dataset.raw ?? "0")) || 0;
el.dataset.raw = String(to);
if (Math.abs(to - from) < Math.pow(10, -decimals)) {
el.textContent = to.toFixed(decimals) + suffix;
return;
}
const start = performance.now();
const dur = 360;
function tick(now) {
const t = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - t, 3);
const v = from + (to - from) * eased;
el.textContent = v.toFixed(decimals) + suffix;
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
// Effective gwei for a given total fee-per-gas (base + tip), capped logic aside.
function feeForGwei(gwei) {
const lum = gwei * GAS_LIMIT * GWEI_TO_LUM;
return { lum, usd: lum * LUM_PRICE_USD };
}
// ---- Render preset cards (gwei + fiat) ----
function renderPresets() {
presetBtns.forEach((btn) => {
const key = btn.dataset.preset;
const p = PRESETS[key];
const gwei = baseFee + p.tip;
const { usd } = feeForGwei(gwei);
const gweiEl = $('[data-field="gwei"]', btn);
const fiatEl = $('[data-field="fiat"]', btn);
animateNum(gweiEl, gwei, 1);
fiatEl.textContent = fmtUsd(usd);
});
}
// ---- Compute current selection (depends on mode) ----
function currentSelection() {
if (mode === "custom") {
const maxBase = Math.max(0, parseFloat(maxBaseEl.value) || 0);
const prio = Math.max(0, parseFloat(priorityEl.value) || 0);
const gwei = maxBase + prio;
let time = "variable";
if (maxBase >= baseFee + 2.5) time = "~15 s";
else if (maxBase >= baseFee + 0.8) time = "~45 s";
else if (maxBase >= baseFee) time = "~2 min";
else time = "may stall";
return { gwei, time };
}
const p = PRESETS[selectedPreset];
return { gwei: baseFee + p.tip, time: p.time };
}
// ---- Render summary + confirm ----
function renderSummary() {
const { gwei, time } = currentSelection();
const { lum, usd } = feeForGwei(gwei);
animateNum(sumGwei, gwei, 1);
sumTime.textContent = time;
animateNum(sumEth, lum, 4);
sumFiat.textContent = "≈ " + fmtUsd(usd);
confirmFee.textContent = fmtUsd(usd);
// Custom underpriced warning
if (mode === "custom") {
const maxBase = parseFloat(maxBaseEl.value) || 0;
if (maxBase < baseFee) {
customWarn.hidden = false;
customWarnText.textContent =
`Max base fee ${maxBase.toFixed(1)} gwei is below the live base fee (${baseFee.toFixed(1)} gwei) — this transaction may stay pending.`;
} else {
customWarn.hidden = true;
}
}
}
function renderAll() {
renderPresets();
renderSummary();
}
// ---- Mode switching ----
function setMode(next) {
mode = next;
modesEl.dataset.active = next;
modeBtns.forEach((b) => {
const on = b.dataset.mode === next;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
panelPresets.classList.toggle("is-hidden", next !== "presets");
panelPresets.hidden = next !== "presets";
panelCustom.classList.toggle("is-hidden", next !== "custom");
panelCustom.hidden = next !== "custom";
renderSummary();
}
modeBtns.forEach((b) => b.addEventListener("click", () => setMode(b.dataset.mode)));
// ---- Preset selection (radiogroup, keyboard) ----
function selectPreset(key, focus) {
selectedPreset = key;
presetBtns.forEach((btn) => {
const on = btn.dataset.preset === key;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-checked", on ? "true" : "false");
btn.tabIndex = on ? 0 : -1;
if (on && focus) btn.focus();
});
renderSummary();
}
presetBtns.forEach((btn) => {
btn.addEventListener("click", () => selectPreset(btn.dataset.preset, false));
btn.addEventListener("keydown", (e) => {
const order = ["slow", "normal", "fast"];
const i = order.indexOf(selectedPreset);
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
e.preventDefault();
selectPreset(order[(i + 1) % order.length], true);
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
e.preventDefault();
selectPreset(order[(i - 1 + order.length) % order.length], true);
}
});
});
// ---- Custom inputs ----
function syncChips() {
const v = parseFloat(priorityEl.value);
chips.forEach((c) => c.classList.toggle("is-active", parseFloat(c.dataset.prio) === v));
}
[maxBaseEl, priorityEl].forEach((el) =>
el.addEventListener("input", () => {
renderSummary();
syncChips();
})
);
chips.forEach((c) =>
c.addEventListener("click", () => {
priorityEl.value = c.dataset.prio;
renderSummary();
syncChips();
})
);
syncBaseBtn.addEventListener("click", () => {
// Sync max base fee to live base fee + a 15% headroom buffer.
const next = +(baseFee * 1.15).toFixed(1);
maxBaseEl.value = next;
renderSummary();
toast(`Max base fee synced to ${next.toFixed(1)} gwei`);
});
// ---- Confirm / sign (simulated) ----
confirmBtn.addEventListener("click", () => {
if (confirmBtn.classList.contains("is-signing")) return;
const { gwei } = currentSelection();
const { usd } = feeForGwei(gwei);
if (mode === "custom" && (parseFloat(maxBaseEl.value) || 0) < baseFee) {
toast("Underpriced — raise max base fee above the live base fee.");
return;
}
confirmBtn.classList.add("is-signing");
const original = confirmLabel.textContent;
confirmLabel.textContent = "Signing…";
toast("Awaiting wallet signature…");
setTimeout(() => {
confirmLabel.textContent = original;
confirmBtn.classList.remove("is-signing");
const hash = "0x" + Math.random().toString(16).slice(2, 6) + "…" +
Math.random().toString(16).slice(2, 6);
toast(`Submitted ${hash} · max fee ${fmtUsd(usd)}`);
}, 1500);
});
// ---- Live base-fee drift ----
function driftBaseFee() {
const prev = baseFee;
// Random walk, gently mean-reverting toward ~21 gwei, bounded.
const drift = (Math.random() - 0.5) * 3.2;
const pull = (21 - baseFee) * 0.08;
baseFee = Math.min(58, Math.max(8, +(baseFee + drift + pull).toFixed(1)));
animateNum(baseFeeEl, baseFee, 1);
if (baseHintEl) baseHintEl.textContent = baseFee.toFixed(1);
tickerEl.classList.remove("is-up", "is-down");
if (baseFee > prev) tickerEl.classList.add("is-up");
else if (baseFee < prev) tickerEl.classList.add("is-down");
renderAll();
}
// ---- Init ----
baseFeeEl.dataset.raw = String(baseFee);
setMode("presets");
selectPreset("normal", false);
syncChips();
renderAll();
setInterval(driftBaseFee, 2400);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web3 — Gas / Network Fee Selector</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=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<section class="card" aria-labelledby="gas-title">
<!-- Header -->
<header class="card__head">
<div class="head__id">
<span class="net-dot" aria-hidden="true"></span>
<div>
<h1 id="gas-title">Network Fee</h1>
<p class="head__sub">Lumen Chain · <span class="mono">EIP-1559</span></p>
</div>
</div>
<div class="ticker" role="status" aria-live="polite" aria-label="Current base fee">
<span class="ticker__label">Base fee</span>
<span class="ticker__val mono" id="baseFee">21.4</span>
<span class="ticker__unit mono">gwei</span>
<span class="ticker__trend" id="baseTrend" aria-hidden="true">▲</span>
</div>
</header>
<!-- Mode switch -->
<div class="modes" role="tablist" aria-label="Fee mode">
<button class="mode is-active" role="tab" aria-selected="true" id="tab-presets" aria-controls="panel-presets" data-mode="presets" type="button">Presets</button>
<button class="mode" role="tab" aria-selected="false" id="tab-custom" aria-controls="panel-custom" data-mode="custom" type="button">Custom</button>
<span class="modes__glow" aria-hidden="true"></span>
</div>
<!-- Presets panel -->
<div class="panel" id="panel-presets" role="tabpanel" aria-labelledby="tab-presets">
<div class="presets" role="radiogroup" aria-label="Transaction speed">
<button class="preset" role="radio" aria-checked="false" data-preset="slow" tabindex="-1" type="button">
<span class="preset__top">
<span class="preset__name">Slow</span>
<span class="preset__time mono">~3 min</span>
</span>
<span class="preset__gwei mono" data-field="gwei">—</span>
<span class="preset__gweiunit">gwei</span>
<span class="preset__fiat mono" data-field="fiat">$0.00</span>
<span class="preset__check" aria-hidden="true"></span>
</button>
<button class="preset is-active" role="radio" aria-checked="true" data-preset="normal" tabindex="0" type="button">
<span class="preset__badge">Recommended</span>
<span class="preset__top">
<span class="preset__name">Normal</span>
<span class="preset__time mono">~45 s</span>
</span>
<span class="preset__gwei mono" data-field="gwei">—</span>
<span class="preset__gweiunit">gwei</span>
<span class="preset__fiat mono" data-field="fiat">$0.00</span>
<span class="preset__check" aria-hidden="true"></span>
</button>
<button class="preset" role="radio" aria-checked="false" data-preset="fast" tabindex="-1" type="button">
<span class="preset__top">
<span class="preset__name">Fast</span>
<span class="preset__time mono">~15 s</span>
</span>
<span class="preset__gwei mono" data-field="gwei">—</span>
<span class="preset__gweiunit">gwei</span>
<span class="preset__fiat mono" data-field="fiat">$0.00</span>
<span class="preset__check" aria-hidden="true"></span>
</button>
</div>
</div>
<!-- Custom panel -->
<div class="panel is-hidden" id="panel-custom" role="tabpanel" aria-labelledby="tab-custom" hidden>
<div class="custom">
<label class="field">
<span class="field__label">Max base fee <span class="field__hint mono">gwei</span></span>
<div class="field__wrap">
<input class="field__input mono" id="maxBase" type="number" min="0" step="0.1" value="24.0" inputmode="decimal" />
<button class="field__sync" id="syncBase" type="button">Sync</button>
</div>
<span class="field__note">Live base fee <span class="mono" id="baseHint">21.4</span> gwei</span>
</label>
<label class="field">
<span class="field__label">Priority fee <span class="field__hint mono">gwei</span></span>
<div class="field__wrap">
<input class="field__input mono" id="priority" type="number" min="0" step="0.1" value="1.5" inputmode="decimal" />
</div>
<div class="prio-presets" aria-label="Priority quick picks">
<button class="chip" type="button" data-prio="1">Low · 1.0</button>
<button class="chip" type="button" data-prio="1.5">Mid · 1.5</button>
<button class="chip" type="button" data-prio="3">High · 3.0</button>
</div>
</label>
<p class="custom__warn" id="customWarn" role="alert" hidden>
<span class="warn__icon" aria-hidden="true">!</span>
<span id="customWarnText">Max base fee is below the live base fee — this transaction may stay pending.</span>
</p>
</div>
</div>
<!-- Summary -->
<section class="summary" aria-label="Fee summary">
<div class="summary__row">
<span class="summary__k">Gas limit</span>
<span class="summary__v mono">21,000</span>
</div>
<div class="summary__row">
<span class="summary__k">Effective fee</span>
<span class="summary__v mono"><span id="sumGwei">—</span> gwei</span>
</div>
<div class="summary__row">
<span class="summary__k">Est. confirm</span>
<span class="summary__v mono" id="sumTime">~45 s</span>
</div>
<div class="summary__row summary__row--total">
<span class="summary__k">Max network fee</span>
<span class="summary__total">
<span class="summary__eth mono" id="sumEth">0.0000</span>
<span class="summary__sym">LUM</span>
<span class="summary__fiat mono" id="sumFiat">≈ $0.00</span>
</span>
</div>
</section>
<!-- Action -->
<footer class="card__foot">
<div class="foot__meta">
<span class="foot__from mono">0x7a3f…c41d</span>
<span class="foot__arrow" aria-hidden="true">→</span>
<span class="foot__to mono">0x91b2…8e07</span>
</div>
<button class="confirm" id="confirmBtn" type="button">
<span class="confirm__label">Confirm & sign</span>
<span class="confirm__fee mono" id="confirmFee">$0.00</span>
</button>
</footer>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Gas / Network Fee Selector
A compact fee-picker card for the fictional Lumen Chain, dressed in the Web3 visual language: a gradient-bordered glassy surface, neon accents, soft glow on the primary action, and monospace numerals for every gwei value, amount and address. A header ticker shows the live base fee and flashes red or green as it moves; addresses like 0x7a3f…c41d are truncated in mono.
The body switches between two modes. Presets offers Slow, Normal and Fast cards — each surfaces its gwei rate, an estimated confirm time (~3 min / ~45 s / ~15 s) and the fiat cost, with the selected card lit by a neon accent and an active-edge marker. The cards form a keyboard-navigable radiogroup, so arrow keys move the selection. Custom swaps in max-base-fee and priority-fee inputs, quick priority chips, a one-tap Sync button that snaps the cap to the live base fee plus headroom, and an inline warning when the cap drops below the current base fee.
Everything feeds a running summary — effective gwei, gas limit, estimated confirmation and a max-network-fee total in LUM plus its fiat estimate — which also drives the Confirm & sign button. A simulated base fee drifts every couple of seconds via a mean-reverting random walk, re-pricing all presets and the summary with animated numbers, and signing runs a mock awaiting-signature state that resolves to a fake transaction hash via a toast. No real wallet, RPC or signing is involved.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.