web design
COMPONENT D / STATUS LINES · BREATHING + TRAVELING SCANLINES
LIVE / DEPLOYED IN PROGRESS ARCHIVED
IMG: B&W · +10 CONTRAST · SCANLINE OVERLAY
GUIDE / STATUS LINES

Overview

Two parts: status squares that breathe in place, each on its own phase, and a scanline panel with sparse vertical travelers drifting across a ruled field. The squares carry real state, the panel is texture.

When to use

Deployment or availability indicators near a project, or a media panel that needs a CRT feel without an image. Use the accent square for one live state per viewport, white for in progress, hollow for archived.

Spec

Tokens
--accent live, --white in progress, --grey-dim archived border
Motion
square opacity yoyo staggered 0.5s, travelers 5 to 14s
A11y
squares decorative, state also in text, reduced-motion static
Perf
traveler start x re-reads clientWidth per pass, no resize hook

Anti-patterns

Do not use the accent square for more than one live state per viewport. Do not add a colored dot to every list row, the dot is for real state only. Keep traveler count low, five is enough.

<!-- Requires GSAP: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js -->

<style>
:root{
  --black:#050505;--white:#FFFFFF;--grey:#A3A3A3;--grey-dim:#6B6B6B;--accent:#22D3EE;
  --line:rgba(255,255,255,.16);--line-faint:rgba(255,255,255,.07);
  --font-display:'Space Grotesk',system-ui,sans-serif;--font-mono:'JetBrains Mono',ui-monospace,monospace;
}
*{margin:0;padding:0;box-sizing:border-box;border-radius:0!important}
body{background:var(--black);color:var(--white);font-family:var(--font-display);min-height:100vh;padding:64px 48px}
.status-squares{display:flex;gap:24px;flex-wrap:wrap;max-width:640px;margin:0 auto 24px}
.status-squares span{font-family:var(--font-mono);font-size:12px;letter-spacing:.06em;color:var(--grey)}
.status-squares i{display:inline-block;width:9px;height:9px;margin-right:8px}
.scan-panel{
  max-width:640px;height:220px;margin:0 auto;border:1px solid var(--line);position:relative;overflow:hidden;
  background:repeating-linear-gradient(0deg,transparent,transparent 5px,var(--line-faint) 5px,var(--line-faint) 6px);
  display:flex;align-items:center;justify-content:center;
}
.scan-panel .cap{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--grey);position:relative;z-index:1}
.traveler{position:absolute;top:0;bottom:0;left:0;width:1px;background:rgba(255,255,255,.12);pointer-events:none}
</style>

<div class="status-squares">
  <span><i class="sq" style="background:var(--accent)"></i>LIVE / DEPLOYED</span>
  <span><i class="sq" style="background:var(--white)"></i>IN PROGRESS</span>
  <span><i class="sq" style="border:1px solid var(--grey-dim)"></i>ARCHIVED</span>
</div>

<div class="scan-panel" id="panel">
  <span class="cap">IMG: B&amp;W · +10 CONTRAST · SCANLINE OVERLAY</span>
</div>

<script>
const mm = gsap.matchMedia();
mm.add({
  full:   "(prefers-reduced-motion: no-preference)",
  reduce: "(prefers-reduced-motion: reduce)"
}, (ctx) => {
  if (!ctx.conditions.full) return;

  // breathing squares - slow opacity pulse, each square offset
  gsap.to(".sq", {
    opacity: 0.3,
    duration: 1.6,
    ease: "sine.inOut",
    repeat: -1,
    yoyo: true,
    stagger: 0.5
  });

  // sparse vertical lines drifting right → left across the panel.
  // repeatRefresh re-rolls the random duration each pass = random separation.
  const panel = document.getElementById("panel");
  for (let i = 0; i < 5; i++) {
    const l = document.createElement("i");
    l.className = "traveler";
    panel.appendChild(l);
    gsap.fromTo(l,
      { x: () => panel.clientWidth + 2 },
      {
        x: -2,
        duration: () => gsap.utils.random(5, 14),
        ease: "none",
        repeat: -1,
        repeatRefresh: true,
        delay: gsap.utils.random(0, 6)
      }
    );
    /* ponytail: start x reads clientWidth per repeat, so travelers self-correct
       after resize within one pass - no resize listener needed */
  }
});
</script>