20 Pure CSS Toggles & Switches
Biometric Scan
A scanline sweeps top-to-bottom, corner brackets glow lilac, and the icon ring lights up when activated. Identity-first UI for auth flows — the toggle becomes a scan target instead of a switch.
Biometric Scan the 4th of 20 designs in the 20 Pure CSS Toggles & Switches collection. The design is implemented in pure CSS — no JavaScript required. Copy the HTML and CSS panels below into your project. Because the demo is pure CSS, it works in any framework or templating engine you happen to use. The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<label class="tg-bio">
<input class="tg-bio-input" type="checkbox" checked>
<span class="tg-bio-frame" aria-hidden="true">
<span class="tg-bio-scanline"></span>
<span class="tg-bio-corners"></span>
<span class="tg-bio-icon-wrap">
<span class="tg-bio-rings"></span>
<svg class="tg-bio-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 11a2 2 0 0 0-2 2v1a8 8 0 0 1-.6 3"/>
<path d="M14 13.1c0 .9 0 1.8-.2 2.6a13 13 0 0 1-.6 2.4"/>
<path d="M16 11.4c.3 1 .4 2 .4 3.1a15 15 0 0 1-.3 2.7"/>
<path d="M18 11.5c.3 2 .3 4 0 6"/>
<path d="M8 13.5c0 1.4-.1 2.7-.5 4"/>
<path d="M6 11c-.2.7-.3 1.4-.3 2.1"/>
<path d="M3.5 9.5A12 12 0 0 1 12 5a12 12 0 0 1 8.5 4.5"/>
</svg>
</span>
<span class="tg-bio-status">Touch ID · scanning</span>
</span>
</label> .tg-bio {
--tg-bio-rim: #14141e;
--tg-bio-wire: #1e1e2e;
--tg-bio-ash: #7a7a98;
--tg-bio-lilac: #c084fc;
display: inline-block;
cursor: pointer;
font-family: "Inter", "Segoe UI", system-ui, sans-serif;
font-size: 14px;
user-select: none;
}
.tg-bio-input {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,0,0);
white-space: nowrap; border: 0;
}
.tg-bio-frame {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
width: 180px;
padding: 20px;
border-radius: 16px;
background: var(--tg-bio-rim);
border: 1px solid var(--tg-bio-wire);
overflow: hidden;
transition: border-color 0.5s ease, background 0.5s ease;
}
.tg-bio-scanline {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--tg-bio-lilac), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.tg-bio-corners {
position: absolute;
inset: 12px;
pointer-events: none;
}
.tg-bio-corners::before,
.tg-bio-corners::after {
content: "";
position: absolute;
width: 14px;
height: 14px;
border-color: var(--tg-bio-wire);
border-style: solid;
transition: border-color 0.4s ease;
}
.tg-bio-corners::before {
top: 0; left: 0;
border-width: 2px 0 0 2px;
border-radius: 3px 0 0 0;
}
.tg-bio-corners::after {
bottom: 0; right: 0;
border-width: 0 2px 2px 0;
border-radius: 0 0 3px 0;
}
.tg-bio-icon-wrap {
position: relative;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1.5px solid var(--tg-bio-wire);
color: var(--tg-bio-ash);
transition: border-color 0.4s ease, color 0.4s ease;
}
.tg-bio-icon {
width: 28px;
height: 28px;
}
/* Two concentric outer rings that light up on scan. The :before adds
a second ring so we get the layered radar look without extra DOM. */
.tg-bio-rings {
position: absolute;
inset: -8px;
border-radius: 50%;
border: 1px solid transparent;
transition: border-color 0.4s ease;
}
.tg-bio-rings::before {
content: "";
position: absolute;
inset: -8px;
border-radius: 50%;
border: 1px solid transparent;
transition: border-color 0.4s ease;
}
.tg-bio-status {
font-size: 12px;
letter-spacing: 0.06em;
color: var(--tg-bio-ash);
transition: color 0.4s ease;
}
@keyframes tg-bio-scan {
0% { top: 0; }
100% { top: 100%; }
}
.tg-bio-input:checked ~ .tg-bio-frame {
border-color: rgba(192,132,252,0.4);
background: rgba(192,132,252,0.05);
}
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-scanline {
opacity: 1;
animation: tg-bio-scan 1.8s ease-in-out infinite;
}
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-icon-wrap {
border-color: var(--tg-bio-lilac);
color: var(--tg-bio-lilac);
}
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-rings {
border-color: rgba(192,132,252,0.3);
}
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-rings::before {
border-color: rgba(192,132,252,0.15);
}
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-status {
color: var(--tg-bio-lilac);
}
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-corners::before,
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-corners::after {
border-color: var(--tg-bio-lilac);
}
.tg-bio-input:focus-visible ~ .tg-bio-frame {
outline: 2px solid var(--tg-bio-lilac);
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.tg-bio-frame,
.tg-bio-icon-wrap,
.tg-bio-rings,
.tg-bio-status,
.tg-bio-corners::before,
.tg-bio-corners::after { transition: none; }
.tg-bio-input:checked ~ .tg-bio-frame .tg-bio-scanline { animation: none; }
}