Guest WiFi Access Card — Free HTML CSS JS Snippet
Guest WiFi Access Card · Cards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="wrap">
<div class="wifi-card">
<div class="wifi-head">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg>
<div>
<h3>Guest WiFi</h3>
<p>Complimentary internet access</p>
</div>
</div>
<div class="wifi-qr" id="wifiQr" aria-label="QR code to join guest WiFi network"></div>
<div class="wifi-field">
<span class="wifi-field-label">Network name (SSID)</span>
<div class="wifi-field-row">
<span class="wifi-field-val">Harbor-Guest</span>
<button class="wifi-copy-btn" data-copy="Harbor-Guest" data-target="ssid">Copy</button>
</div>
</div>
<div class="wifi-field">
<span class="wifi-field-label">Password</span>
<div class="wifi-field-row">
<span class="wifi-field-val" id="wifiPassText">••••••••••</span>
<button class="wifi-eye-btn" id="wifiEye" aria-label="Show password">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button class="wifi-copy-btn" data-copy="Sunset&2024!" data-target="pass">Copy</button>
</div>
</div>
<p class="wifi-note" id="wifiNote">Password hidden — tap the eye icon to reveal it before sharing.</p>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 20px; }
.wrap { width: 100%; max-width: 340px; }
.wifi-card { background: #fff; border-radius: 20px; padding: 22px; box-shadow: 0 18px 44px rgba(15,23,42,0.1); border: 1px solid #f1f5f9; }
.wifi-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
.wifi-head svg { color: #6366f1; flex-shrink: 0; }
.wifi-head h3 { font-size: 15.5px; font-weight: 800; color: #0f172a; }
.wifi-head p { font-size: 12px; color: #94a3b8; margin-top: 1px; }
.wifi-qr { width: 148px; height: 148px; margin: 0 auto 18px; border-radius: 14px; background: #fff; border: 1px solid #f1f5f9; display: grid; grid-template-columns: repeat(21, 1fr); grid-template-rows: repeat(21, 1fr); padding: 10px; gap: 1px; }
.qr-cell { border-radius: 1px; }
.qr-cell.on { background: #0f172a; }
.wifi-field { margin-bottom: 14px; }
.wifi-field-label { font-size: 10.5px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.04em; display: block; margin-bottom: 6px; }
.wifi-field-row { display: flex; align-items: center; gap: 8px; background: #f8fafc; border-radius: 10px; padding: 10px 12px; }
.wifi-field-val { flex: 1; font-size: 14px; font-weight: 700; color: #0f172a; font-family: ui-monospace, 'SF Mono', monospace; letter-spacing: 0.02em; }
.wifi-eye-btn { border: none; background: transparent; color: #94a3b8; cursor: pointer; display: flex; padding: 2px; flex-shrink: 0; transition: color 0.12s; }
.wifi-eye-btn:hover { color: #6366f1; }
.wifi-copy-btn { border: none; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.08); color: #4f46e5; font-size: 11.5px; font-weight: 700; padding: 6px 11px; border-radius: 7px; cursor: pointer; flex-shrink: 0; transition: background 0.12s, color 0.12s; }
.wifi-copy-btn:hover { background: #eef2ff; }
.wifi-copy-btn.copied { background: #dcfce7; color: #15803d; }
.wifi-note { font-size: 11.5px; color: #94a3b8; text-align: center; line-height: 1.5; transition: color 0.15s; }
.wifi-note.revealed { color: #16a34a; font-weight: 600; }// Deterministic pseudo-random QR-style pattern generator so the SVG-free grid
// always renders the same "scannable-looking" pattern for a given seed string.
function seededPattern(seed, size) {
var hash = 0;
for (var i = 0; i < seed.length; i++) { hash = (hash * 31 + seed.charCodeAt(i)) >>> 0; }
var cells = [];
for (var r = 0; r < size; r++) {
for (var c = 0; c < size; c++) {
hash = (hash * 1103515245 + 12345) >>> 0;
var isFinderCorner = (r < 6 && c < 6) || (r < 6 && c >= size - 6) || (r >= size - 6 && c < 6);
cells.push(isFinderCorner ? finderPattern(r, c, size) : ((hash >> 9) & 1) === 1);
}
}
return cells;
}
function finderPattern(r, c, size) {
var localR = r < 6 ? r : (c < 6 ? r - (size - 6) : r);
var localC = c < 6 ? c : c - (size - 6);
var ring = Math.max(Math.abs(localR - 2.5), Math.abs(localC - 2.5));
return ring < 1 || (ring >= 2 && ring < 3);
}
function renderQr() {
var size = 21;
var pattern = seededPattern('Harbor-Guest|Sunset&2024!', size);
var wrap = document.getElementById('wifiQr');
wrap.innerHTML = '';
pattern.forEach(function (on) {
var cell = document.createElement('div');
cell.className = 'qr-cell' + (on ? ' on' : '');
wrap.appendChild(cell);
});
}
function copyText(text, btn) {
var restore = btn.textContent;
function done(ok) {
btn.textContent = ok ? 'Copied!' : 'Failed';
btn.classList.toggle('copied', ok);
setTimeout(function () { btn.textContent = restore; btn.classList.remove('copied'); }, 1600);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); });
} else {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(ta);
done(ok);
}
}
document.querySelectorAll('.wifi-copy-btn').forEach(function (btn) {
btn.addEventListener('click', function () { copyText(btn.dataset.copy, btn); });
});
var revealed = false;
var REAL_PASSWORD = 'Sunset&2024!';
var eyeBtn = document.getElementById('wifiEye');
var passText = document.getElementById('wifiPassText');
var note = document.getElementById('wifiNote');
eyeBtn.addEventListener('click', function () {
revealed = !revealed;
passText.textContent = revealed ? REAL_PASSWORD : '••••••••••';
eyeBtn.setAttribute('aria-label', revealed ? 'Hide password' : 'Show password');
note.textContent = revealed
? 'Password visible — hide it again once your guest has connected.'
: 'Password hidden — tap the eye icon to reveal it before sharing.';
note.classList.toggle('revealed', revealed);
});
renderQr();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Guest WiFi Access Card — QR Pattern, Masked Password Reveal & Copy-to-Clipboard

The universal ritual of asking a host for the WiFi password — squinting at a router sticker, typing a long mixed-case string on a phone keyboard — is exactly what this card exists to replace. It presents a guest network's SSID and password as a compact, shareable card: a scannable QR pattern for one-tap join, a masked password that stays hidden by default, and copy buttons that put either value on the clipboard without anyone needing to read or retype it.
A deterministic QR-style pattern without a QR library
seededPattern() hashes the SSID-and-password string into a numeric seed, then walks a 21×21 grid deriving each cell's on/off state from that running hash — plus a dedicated finderPattern() function that draws the three square "finder" corner markers real QR codes use to help a scanner locate and orient the code. This produces a visually authentic-looking, deterministic pattern (the same credentials always render the same grid) without pulling in a QR-encoding library. For production use where the pattern must actually decode as a scannable QR code, swap renderQr() for a real QR-encoding library that encodes a WIFI:T:WPA;S:<ssid>;P:<password>;; payload string — the rest of the card (copy buttons, reveal toggle) works identically either way.
Password hidden by default, revealed on demand
wifiPassText shows a fixed-width bullet mask until the eye-icon button is clicked, at which point revealed flips and the real REAL_PASSWORD constant is swapped in. This matters for any shared or public-facing screen (a lobby kiosk, a printed card on a cafe counter, a wall-mounted tablet) — a password visible at all times to anyone glancing at the screen defeats the point of having one, so requiring a deliberate tap to reveal it adds a small but real layer of intent before it's exposed.
Copy buttons that work with or without the async Clipboard API
copyText() tries navigator.clipboard.writeText() first (the modern async Clipboard API), but falls back to creating an off-screen <textarea>, selecting its contents, and calling the older document.execCommand('copy') if the async API isn't available — some embedded browsers, older devices, and non-HTTPS contexts still lack Clipboard API support, and a guest WiFi card is exactly the kind of interface likely to be viewed from an unpredictable range of guest devices rather than a controlled internal environment.
Feedback that confirms without demanding attention
Each copy button briefly relabels itself "Copied!" (with a green tint) for 1.6 seconds before reverting, rather than opening a toast or alert — a guest who copied the password already knows what they copied; they just need confirmation the click registered before switching to their WiFi settings screen to paste it in.
Step by step
How to Use
- 1Set the real network credentialsUpdate the SSID text in the HTML, the data-copy attribute on the SSID copy button, and REAL_PASSWORD plus its matching data-copy attribute in the JS.
- 2Tap the eye icon to reveal the passwordThe password stays masked with bullet characters by default; clicking the eye icon swaps in the real value and updates the helper note.
- 3Click Copy on either fieldThe SSID or password is copied to the clipboard, with the button briefly showing "Copied!" as confirmation.
- 4Swap in a real QR encoder for productionReplace renderQr() with a proper QR-encoding library call that encodes a WIFI:T:WPA;S:<ssid>;P:<password>;; payload string so phone cameras can actually auto-join.
- 5Style for your venueUpdate the header text, icon, and colors to match your hotel, cafe, office, or venue branding.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a Tailwind CSS version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The demo pattern is a deterministic, authentic-looking grid generated from a hash of the credentials — it is not a real encoded QR code and will not decode. For production, replace renderQr() with a proper QR-encoding library that encodes a WIFI:T:WPA;S:<ssid>;P:<password>;; payload string, which is the standard format phone cameras recognize as WiFi-join QR codes.
A guest WiFi card is often displayed on a shared or public screen (a lobby kiosk, a printed card, a wall tablet). Hiding the password by default and requiring a deliberate eye-icon tap to reveal it adds a small layer of intent before the credential is exposed to anyone glancing at the screen.
copyText() falls back to creating an off-screen textarea containing the value, selecting its text, and calling the older document.execCommand("copy") API. This covers older browsers, some embedded WebViews, and non-HTTPS contexts where the modern async Clipboard API is unavailable.
Update the data-copy attribute on each .wifi-copy-btn button to the value you want copied, and keep the REAL_PASSWORD constant and the visible SSID text in sync with the same values so the displayed and copied credentials always match.
Yes — since the card reads REAL_PASSWORD from a single JavaScript constant and the QR pattern is regenerated from the same credential string via renderQr(), updating that one constant (and re-running renderQr()) on a schedule from your access point's admin API keeps both the displayed password and the QR pattern in sync automatically.
In React, keep revealed as useState(false) and toggle it in the eye button's onClick, deriving the displayed password text from a ternary; keep the copy-to-clipboard function as a plain async helper called from each button's onClick with its own value. In Vue, use a ref for revealed and a computed property for the displayed password text.