---
name: scanner-line-effect
description: Use when you want "Techy, security, futuristic" - Moves a line across content to simulate scanning.
---

# Scanner Line Effect

> **Category:** Premium / Futuristic  -  **Personality:** Techy, security, futuristic
>
> **Best use:** Cybersecurity, AI analysis, loading states
>
> **Ratings:** Professional 3/5 - Casual 3/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A bright, glowing line sweeps across an image or panel to imply that software is *scanning and analysing* it — a biometric face scan, a document reader, or an AI "thinking" pass. The line trails a soft glow and, crucially, **reveals annotations as it passes**: bounding boxes, landmark crosshairs, and a readout that resolves field by field. It is the visual shorthand for "the machine is looking", which is why it suits security gates, verification screens, and loading states. The whole sweep is one CSS keyframe; JavaScript only orchestrates the readout and the **Re-scan** replay.

## Dependencies / CDN

None - pure CSS for the effect (vanilla JS only for the readout/replay). The demo uses two Google Fonts (optional):

```html
<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=Chakra+Petch:wght@500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
```

## HTML

A framed viewport holds the image, the analysis overlay, the marks, and the line. The state class (`is-scanning` / `is-analyzed`) lives on an ancestor so every layer reacts together.

```html
<div class="scan">                              <!-- state holder -->
  <figure class="view">
    <img class="photo" src="/img/portrait.jpg" alt="Subject being verified">

    <!-- analysis overlay: clip-wiped open in sync with the line -->
    <div class="reveal" aria-hidden="true"><div class="mesh"></div></div>

    <!-- marks that "focus in" as the line crosses them (--y = vertical position 0..1) -->
    <div class="marks" aria-hidden="true">
      <span class="bbox pop" style="--y:.16"></span>
      <span class="node pop" style="--y:.31;left:42%;top:31%"><span class="x"></span><b>L·EYE</b></span>
      <span class="node pop" style="--y:.31;left:61%;top:31%"><span class="x"></span><b>R·EYE</b></span>
    </div>

    <span class="line" aria-hidden="true"></span>         <!-- THE scanner line -->
    <span class="status" aria-live="polite">Analyzing</span>
  </figure>

  <button class="rescan" type="button">Re-scan</button>
</div>
```

## CSS

```css
.scan{--dur:2.8s;--cyan:#2af2e4;--glow:#00ffe6;--green:#5ef0a0}
.view{position:relative;overflow:hidden;border-radius:14px;aspect-ratio:2/3;background:#04080f}
.photo{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

/* 1) THE LINE — a thin glowing gradient bar with a comet trail above it */
.line{position:absolute;left:-2%;right:-2%;top:-2%;height:2px;opacity:0;z-index:6;
  background:linear-gradient(90deg,transparent,var(--cyan) 16%,#eafffd 50%,var(--cyan) 84%,transparent);
  box-shadow:0 0 10px var(--glow),0 0 30px rgba(0,255,230,.6)}
.line::before{content:"";position:absolute;left:0;right:0;bottom:2px;height:130px;   /* trailing glow */
  background:linear-gradient(to top,rgba(0,255,230,.32),transparent 70%);
  -webkit-mask:linear-gradient(to top,#000,transparent);mask:linear-gradient(to top,#000,transparent)}
.scan.is-scanning .line{opacity:1;animation:sweep var(--dur) linear forwards}
@keyframes sweep{from{top:-2%}to{top:102%}}

/* 2) THE REVEAL — an analysis layer wiped open with the SAME duration/timing as the line */
.reveal{position:absolute;inset:0;z-index:3;opacity:0;clip-path:inset(0 0 101% 0)}
.mesh{position:absolute;inset:0;mix-blend-mode:screen;
  background:
    repeating-linear-gradient(0deg ,transparent 0 17px,rgba(42,242,228,.16) 17px 18px),
    repeating-linear-gradient(90deg,transparent 0 17px,rgba(42,242,228,.16) 17px 18px)}
.scan.is-scanning .reveal{opacity:1;animation:wipe var(--dur) linear forwards}
.scan.is-analyzed .reveal{opacity:1;clip-path:inset(0 0 0 0)}
@keyframes wipe{from{clip-path:inset(0 0 101% 0)}to{clip-path:inset(0 0 0 0)}}

/* 3) THE MARKS — pop into focus exactly when the line reaches their --y position */
.pop{opacity:0}
.scan.is-scanning .pop{animation:pop .5s ease-out both;animation-delay:calc(var(--y) * var(--dur))}
.scan.is-analyzed .pop{opacity:1}
@keyframes pop{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:blur(0)}}

.bbox{position:absolute;top:16%;left:28%;right:26%;bottom:47%;border:1.5px solid var(--cyan);border-radius:9px;
  box-shadow:0 0 14px rgba(0,255,230,.35);transition:border-color .45s}
.scan.is-analyzed .bbox{border-color:var(--green)}      /* turns green = "locked" */

/* no sweep for reduced-motion users — show the finished analysed state */
@media (prefers-reduced-motion:reduce){
  .line{display:none}
  .reveal{opacity:1;clip-path:inset(0 0 0 0)}
  .pop{opacity:1;filter:none}
  .bbox{border-color:var(--green)}
}
```

## JavaScript

Optional — the sweep is pure CSS. JS only sequences the readout to match the line and lets the user replay. Gate the sweep behind `prefers-reduced-motion`.

```js
(function(){
  var scan = document.querySelector('.scan'); if(!scan) return;
  var btn  = scan.querySelector('.rescan');
  var DUR  = 2800;
  var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  var timers = [];

  function run(){
    timers.forEach(clearTimeout); timers = [];
    scan.classList.remove('is-analyzed','is-scanning');
    void scan.offsetWidth;                       // reflow → restart the CSS animations

    if(reduce){ scan.classList.add('is-analyzed'); return; }   // no sweep

    scan.classList.add('is-scanning');
    // …resolve readout rows on a schedule tied to DUR (e.g. setTimeout at 34%, 54%, …)…
    timers.push(setTimeout(function(){
      scan.classList.replace('is-scanning','is-analyzed');     // settle when the sweep ends
    }, DUR + 70));
  }

  if(btn) btn.addEventListener('click', run);
  run();                                         // auto-play once
})();
```

## How it works

- **The line is one element animated on `top` 0→100%.** Its `box-shadow` is the glow; a tall `::before` gradient *above* it (it travels downward) is the comet **trail**, faded with a `mask`.
- **The reveal is synchronised, not scripted.** The analysis layer (`.reveal`) animates `clip-path: inset(0 0 101% 0)` → `inset(0 0 0 0)` with the **same `--dur` and `linear` timing** as the line, so its open edge sits exactly under the line as it descends. Linear timing is what keeps line, wipe, and marks aligned.
- **Marks "focus in" on cue.** Each mark carries `--y` (its vertical position as 0–1). `animation-delay: calc(var(--y) * var(--dur))` fires its blur-to-sharp `pop` at the instant the line crosses it — pure CSS, no per-element JS.
- **State lives on an ancestor.** Toggling `is-scanning` / `is-analyzed` on `.scan` drives every layer; `forwards` fill plus an explicit `is-analyzed` rule holds the finished frame.

## Customization

- **Speed:** change `--dur` once; line, wipe, and every mark delay follow automatically.
- **Colour / threat-state:** swap `--cyan`/`--green` (e.g. amber while scanning, green on pass, red on reject).
- **Trail length & intensity:** the `::before` `height` (130px) and its alpha. Bigger `box-shadow` = thicker laser.
- **Direction:** animate `left` instead of `top` (and rotate the trail) for a horizontal sweep; change `clip-path` inset side to match.
- **Loop vs one-shot:** add `infinite` to the `sweep`/`wipe` keyframes for a perpetual loading scanner; keep `forwards` (as here) for a verify-once gate with a Re-scan button.

## Accessibility & performance

- **Honour `prefers-reduced-motion`** in CSS *and* in JS (`matchMedia`): show the analysed end-state with no moving line, per the demo.
- **Cheap:** it animates only `top` / `clip-path` / `opacity` (compositor-friendly) on a handful of nodes — no canvas, no WebGL, no layout thrash.
- Mark decorative layers `aria-hidden="true"`; keep the result text as **real text** and put the status in an `aria-live="polite"` region so screen readers hear "Analyzing → Verified".
- The image needs a meaningful `alt`; the **Re-scan** control is a real `<button>` (keyboard + focus-visible).

## Gotchas

- **Keep the timing functions identical and `linear`.** An eased line over a linearly-wiped reveal (or vice-versa) drifts out of sync; `animation-delay` math for the marks also assumes constant speed.
- **Pop without transforms on positioned marks.** Marks centred with `transform:translate(-50%,-50%)` will jump if the `pop` keyframe also animates `transform` — animate `opacity`/`filter` only (a blur→sharp "focus" reads better here anyway).
- **Restarting needs a reflow.** To replay, remove the state class, force layout (`void el.offsetWidth`), then re-add it — otherwise the CSS animation won't re-run.
- **`overflow:hidden` on the frame** clips the line, trail, and glow to the viewport. Pull the line slightly past the edges (`left:-2%;right:-2%`) so its end caps never reveal a hard stop.
