Wi-FiOn
Wi-Fi
import { useEffect, useRef, useState } from 'react';
const NETS = [
{ name: 'Studio', bars: 3, lock: true },
{ name: 'Home 5G', bars: 3, lock: true },
{ name: 'Cafe Guest', bars: 2, lock: false },
{ name: 'Printer 4F', bars: 1, lock: true },
];
const ROW = 38;
const css = `
.card-wifi { box-sizing: border-box; width: 240px; padding: 12px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-wifi__head { display: flex; align-items: center; justify-content: space-between; margin: 0 4px 8px; font-size: 13px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); }
.card-wifi__head span { font-size: 11px; font-weight: 500; color: light-dark(#5f626a, #a7abb2); }
.card-wifi__list { position: relative; height: ${NETS.length * ROW}px; margin: 0; padding: 0; list-style: none; }
.card-wifi__item { position: absolute; left: 0; right: 0; top: 0; transition: transform 0.5s cubic-bezier(0.3, 1.25, 0.5, 1); }
.card-wifi__row { display: flex; align-items: center; gap: 10px; box-sizing: border-box; width: 100%; height: ${ROW - 4}px; padding: 0 8px; border: 0; border-radius: 9px; background: none; color: light-dark(#42454c, #e5e6e9); font: inherit; font-size: 12px; text-align: left; cursor: pointer; transition: background-color 0.2s; }
.card-wifi__row:hover { background: light-dark(#ebedf0, #232529); }
.card-wifi__row:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: -1px; }
.card-wifi__item--on .card-wifi__row { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); font-weight: 600; cursor: default; }
.card-wifi__signal { flex: none; width: 18px; height: 14px; }
.card-wifi__arc { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 2; stroke-linecap: round; }
.card-wifi__arc--lit { stroke: light-dark(#373a40, #f1f2f3); }
.card-wifi__dot { fill: light-dark(#373a40, #f1f2f3); }
/* While it connects the arcs light in a wave from the dot outwards. */
.card-wifi__item--busy .card-wifi__arc { stroke: light-dark(#373a40, #f1f2f3); animation: card-wifi-wave 0.9s ease-in-out infinite; }
.card-wifi__item--busy .card-wifi__arc:nth-of-type(2) { animation-delay: 0.15s; }
.card-wifi__item--busy .card-wifi__arc:nth-of-type(3) { animation-delay: 0.3s; }
@keyframes card-wifi-wave {
0%, 100% { opacity: 0.2; }
40% { opacity: 1; }
}
.card-wifi__status { margin-left: auto; font-size: 10.5px; font-weight: 500; color: light-dark(#5f626a, #a7abb2); }
.card-wifi__status svg { display: block; width: 14px; height: 14px; color: light-dark(#373a40, #f1f2f3); }
.card-wifi__lock { width: 11px; height: 11px; color: light-dark(#6d7179, #7d8087); }
.card-wifi__tick path { stroke-dasharray: 1; animation: card-wifi-tick 0.35s ease-out backwards; }
@keyframes card-wifi-tick {
from { stroke-dashoffset: 1; }
}
@media (prefers-reduced-motion: reduce) {
.card-wifi__item { transition: none; }
.card-wifi__item--busy .card-wifi__arc, .card-wifi__tick path { animation: none; }
}
`;
function Signal({ bars }: { bars: number }) {
return (
<svg className="card-wifi__signal" viewBox="0 0 18 14" aria-hidden="true">
<circle className="card-wifi__dot" cx={9} cy={12} r={1.5} />
{[4, 7.5, 11].map((r, i) => (
<path key={r} className={`card-wifi__arc${i < bars ? ' card-wifi__arc--lit' : ''}`} d={`M ${9 - r * 0.72} ${12 - r * 0.7} A ${r} ${r} 0 0 1 ${9 + r * 0.72} ${12 - r * 0.7}`} />
))}
</svg>
);
}
export function WifiCard() {
const [order, setOrder] = useState(NETS.map((n) => n.name));
const [busy, setBusy] = useState<string | null>(null);
const timer = useRef(0);
useEffect(() => () => clearTimeout(timer.current), []);
const live = order[0];
// Joining a network: it pulses while it connects, then ticks and moves to the top as the rest shift down.
const join = (name: string) => {
if (name === live || busy) return;
setBusy(name);
timer.current = window.setTimeout(() => {
setBusy(null);
setOrder((o) => [name, ...o.filter((n) => n !== name)]);
}, 1300);
};
return (
<>
<style>
{css}
</style>
<article className="card-wifi">
<p className="card-wifi__head">
Wi-Fi<span>On</span>
</p>
<ul className="card-wifi__list">
{NETS.map((n) => {
const at = order.indexOf(n.name);
const on = n.name === live && !busy;
const state = n.name === busy ? 'Connecting…' : n.name === live ? 'Connected' : '';
return (
<li key={n.name} className={`card-wifi__item${n.name === live ? ' card-wifi__item--on' : ''}${n.name === busy ? ' card-wifi__item--busy' : ''}`} style={{ transform: `translateY(${at * ROW}px)` }}>
<button type="button" className="card-wifi__row" onClick={() => join(n.name)} aria-label={`${n.name}${state ? `, ${state}` : ''}${n.lock ? ', secured' : ''}`}>
<Signal bars={n.bars} />
{n.name}
<span className="card-wifi__status" aria-hidden="true">
{n.name === busy ? (
'Connecting…'
) : on ? (
<svg className="card-wifi__tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12.5l4.5 4.5L19 7.5" pathLength={1} />
</svg>
) : n.lock ? (
<svg className="card-wifi__lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<rect x={5} y={11} width={14} height={10} rx={2} />
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
</svg>
) : null}
</span>
</button>
</li>
);
})}
</ul>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardWifi(el) on the mounted element instead.
<article class="card-wifi">
<p class="card-wifi__head">
Wi-Fi
<span>On</span>
</p>
<ul class="card-wifi__list">
<li class="card-wifi__item card-wifi__item--on" style="transform:translateY(0px)">
<button type="button" class="card-wifi__row" aria-label="Studio, Connected, secured">
<svg class="card-wifi__signal" viewBox="0 0 18 14" aria-hidden="true">
<circle class="card-wifi__dot" cx="9" cy="12" r="1.5"></circle>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 6.12 9.2 A 4 4 0 0 1 11.88 9.2"></path>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 3.6 6.75 A 7.5 7.5 0 0 1 14.4 6.75"></path>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 1.08 4.3 A 11 11 0 0 1 16.92 4.3"></path>
</svg>
Studio
<span class="card-wifi__status" aria-hidden="true">
<svg class="card-wifi__tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"></path>
</svg>
</span>
</button>
</li>
<li class="card-wifi__item" style="transform:translateY(38px)">
<button type="button" class="card-wifi__row" aria-label="Home 5G, secured">
<svg class="card-wifi__signal" viewBox="0 0 18 14" aria-hidden="true">
<circle class="card-wifi__dot" cx="9" cy="12" r="1.5"></circle>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 6.12 9.2 A 4 4 0 0 1 11.88 9.2"></path>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 3.6 6.75 A 7.5 7.5 0 0 1 14.4 6.75"></path>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 1.08 4.3 A 11 11 0 0 1 16.92 4.3"></path>
</svg>
Home 5G
<span class="card-wifi__status" aria-hidden="true">
<svg class="card-wifi__lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="11" width="14" height="10" rx="2"></rect>
<path d="M8 11V8a4 4 0 0 1 8 0v3"></path>
</svg>
</span>
</button>
</li>
<li class="card-wifi__item" style="transform:translateY(76px)">
<button type="button" class="card-wifi__row" aria-label="Cafe Guest">
<svg class="card-wifi__signal" viewBox="0 0 18 14" aria-hidden="true">
<circle class="card-wifi__dot" cx="9" cy="12" r="1.5"></circle>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 6.12 9.2 A 4 4 0 0 1 11.88 9.2"></path>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 3.6 6.75 A 7.5 7.5 0 0 1 14.4 6.75"></path>
<path class="card-wifi__arc" d="M 1.08 4.3 A 11 11 0 0 1 16.92 4.3"></path>
</svg>
Cafe Guest
<span class="card-wifi__status" aria-hidden="true"></span>
</button>
</li>
<li class="card-wifi__item" style="transform:translateY(114px)">
<button type="button" class="card-wifi__row" aria-label="Printer 4F, secured">
<svg class="card-wifi__signal" viewBox="0 0 18 14" aria-hidden="true">
<circle class="card-wifi__dot" cx="9" cy="12" r="1.5"></circle>
<path class="card-wifi__arc card-wifi__arc--lit" d="M 6.12 9.2 A 4 4 0 0 1 11.88 9.2"></path>
<path class="card-wifi__arc" d="M 3.6 6.75 A 7.5 7.5 0 0 1 14.4 6.75"></path>
<path class="card-wifi__arc" d="M 1.08 4.3 A 11 11 0 0 1 16.92 4.3"></path>
</svg>
Printer 4F
<span class="card-wifi__status" aria-hidden="true">
<svg class="card-wifi__lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="11" width="14" height="10" rx="2"></rect>
<path d="M8 11V8a4 4 0 0 1 8 0v3"></path>
</svg>
</span>
</button>
</li>
</ul>
</article>.card-wifi { box-sizing: border-box; width: 240px; padding: 12px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-wifi__head { display: flex; align-items: center; justify-content: space-between; margin: 0 4px 8px; font-size: 13px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); }
.card-wifi__head span { font-size: 11px; font-weight: 500; color: light-dark(#5f626a, #a7abb2); }
.card-wifi__list { position: relative; height: 152px; margin: 0; padding: 0; list-style: none; }
.card-wifi__item { position: absolute; left: 0; right: 0; top: 0; transition: transform 0.5s cubic-bezier(0.3, 1.25, 0.5, 1); }
.card-wifi__row { display: flex; align-items: center; gap: 10px; box-sizing: border-box; width: 100%; height: 34px; padding: 0 8px; border: 0; border-radius: 9px; background: none; color: light-dark(#42454c, #e5e6e9); font: inherit; font-size: 12px; text-align: left; cursor: pointer; transition: background-color 0.2s; }
.card-wifi__row:hover { background: light-dark(#ebedf0, #232529); }
.card-wifi__row:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: -1px; }
.card-wifi__item--on .card-wifi__row { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); font-weight: 600; cursor: default; }
.card-wifi__signal { flex: none; width: 18px; height: 14px; }
.card-wifi__arc { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 2; stroke-linecap: round; }
.card-wifi__arc--lit { stroke: light-dark(#373a40, #f1f2f3); }
.card-wifi__dot { fill: light-dark(#373a40, #f1f2f3); }
/* While it connects the arcs light in a wave from the dot outwards. */
.card-wifi__item--busy .card-wifi__arc { stroke: light-dark(#373a40, #f1f2f3); animation: card-wifi-wave 0.9s ease-in-out infinite; }
.card-wifi__item--busy .card-wifi__arc:nth-of-type(2) { animation-delay: 0.15s; }
.card-wifi__item--busy .card-wifi__arc:nth-of-type(3) { animation-delay: 0.3s; }
@keyframes card-wifi-wave {
0%, 100% { opacity: 0.2; }
40% { opacity: 1; }
}
.card-wifi__status { margin-left: auto; font-size: 10.5px; font-weight: 500; color: light-dark(#5f626a, #a7abb2); }
.card-wifi__status svg { display: block; width: 14px; height: 14px; color: light-dark(#373a40, #f1f2f3); }
.card-wifi__lock { width: 11px; height: 11px; color: light-dark(#6d7179, #7d8087); }
.card-wifi__tick path { stroke-dasharray: 1; animation: card-wifi-tick 0.35s ease-out backwards; }
@keyframes card-wifi-tick {
from { stroke-dashoffset: 1; }
}
@media (prefers-reduced-motion: reduce) {
.card-wifi__item { transition: none; }
.card-wifi__item--busy .card-wifi__arc, .card-wifi__tick path { animation: none; }
}function initCardWifi(root) {
var NETS = [
{ name: 'Studio', lock: true },
{ name: 'Home 5G', lock: true },
{ name: 'Cafe Guest', lock: false },
{ name: 'Printer 4F', lock: true },
];
var ROW = 38;
var TICK =
'<svg class="card-wifi__tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"></path></svg>';
var LOCK =
'<svg class="card-wifi__lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
'<rect x="5" y="11" width="14" height="10" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg>';
var items = root.querySelectorAll('.card-wifi__item');
var order = NETS.map(function (n) {
return n.name;
});
var busy = null;
var timer = 0;
function kind(n) {
return n.name === busy ? 'busy' : n.name === order[0] ? 'on' : n.lock ? 'lock' : '';
}
var kinds = NETS.map(kind);
function render() {
var live = order[0];
items.forEach(function (item, k) {
var n = NETS[k];
var state = n.name === busy ? 'Connecting…' : n.name === live ? 'Connected' : '';
item.style.transform = 'translateY(' + order.indexOf(n.name) * ROW + 'px)';
item.classList.toggle('card-wifi__item--on', n.name === live);
item.classList.toggle('card-wifi__item--busy', n.name === busy);
item.querySelector('.card-wifi__row').setAttribute('aria-label', n.name + (state ? ', ' + state : '') + (n.lock ? ', secured' : ''));
// Only a change of state redraws the status, so the tick draws once, when it arrives.
var next = kind(n);
if (next === kinds[k]) return;
kinds[k] = next;
item.querySelector('.card-wifi__status').innerHTML = next === 'busy' ? 'Connecting…' : next === 'on' ? TICK : next === 'lock' ? LOCK : '';
});
}
// Joining a network: it pulses while it connects, then ticks and moves to the top as the rest shift down.
function onPick(e) {
var name = NETS[Array.prototype.indexOf.call(items, e.currentTarget.parentNode)].name;
if (name === order[0] || busy) return;
busy = name;
render();
timer = setTimeout(function () {
busy = null;
order = [name].concat(
order.filter(function (n) {
return n !== name;
}),
);
render();
}, 1300);
}
var btns = root.querySelectorAll('.card-wifi__row');
btns.forEach(function (b) {
b.addEventListener('click', onPick);
});
return function () {
clearTimeout(timer);
btns.forEach(function (b) {
b.removeEventListener('click', onPick);
});
};
}
document.querySelectorAll('.card-wifi').forEach(initCardWifi);Goal: Create a "Wi-Fi" card.
Visual behavior: A network picker: choose a network and its signal arcs pulse while it connects, then it ticks and moves to the top of the list as the others shift down.
Technique:
- The rows are absolutely placed and moved by translateY from their place in an order list, so reordering is one change and every row glides to its new spot.
- While connecting, the signal's three arcs pulse in a wave from the dot outward (one keyframe with staggered delays).
- Once connected the tick draws in by its dash offset; only a change of state redraws the status, so it draws once.
Accessibility: Each row is a button whose label gives the network, its state (Connecting, Connected) and whether it is secured; the icons are aria-hidden. Under prefers-reduced-motion the rows jump to their places and the arcs hold still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Wi-Fi" card.
Visual behavior: A network picker: choose a network and its signal arcs pulse while it connects, then it ticks and moves to the top of the list as the others shift down.
Technique:
- The rows are absolutely placed and moved by translateY from their place in an order list, so reordering is one change and every row glides to its new spot.
- While connecting, the signal's three arcs pulse in a wave from the dot outward (one keyframe with staggered delays).
- Once connected the tick draws in by its dash offset; only a change of state redraws the status, so it draws once.
Accessibility: Each row is a button whose label gives the network, its state (Connecting, Connected) and whether it is secured; the icons are aria-hidden. Under prefers-reduced-motion the rows jump to their places and the arcs hold still.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A network picker: choose a network and its signal arcs pulse while it connects, then it ticks and moves to the top of the list as the others shift down.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, loop, card
- Techniques
- keyframes, transform only, stroke dashoffset
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
Each row is a button whose label gives the network, its state (Connecting, Connected) and whether it is secured; the icons are aria-hidden. Under prefers-reduced-motion the rows jump to their places and the arcs hold still.