Light and motion effects
Small classes that give any block a sense of light: a glint that runs along the border once, a border that glows where the pointer comes near, a scan line crossing a large surface, sparks, a breathing halo, and elements that reveal as they scroll into view. Everything is CSS except two tiny components: Proximity, which feeds the pointer position into custom properties, and Reveal, which marks elements when they enter the viewport. Under reduced motion nothing moves and nothing is hidden.
Edge glint and proximity
iv-edge-glint sends a light around the border on hover, on focus within, or when the element is revealed. iv-edge-near lights the border where the pointer approaches; it needs a Proximity root above it (by default the whole page) and a fine pointer.
<section id="effects-edges" class="iv-stack iv-u-gap-6" style="padding: clamp(2rem, 6vw, 4rem); border-radius: var(--iv-radius-lg); background-color: var(--iv-color-surface)">
<p class="iv-u-m-0">Made-up cards, for the fixture only. Hover the first card to send a glint around its border; move the pointer near the second one to light the edge closest to you. The proximity glow needs JavaScript and a fine pointer: with a touch screen, or with scripting off, the cards keep their plain border.</p>
<div class="iv-grid iv-grid--auto iv-u-gap-6">
<article class="iv-card iv-edge-glint">
<div class="iv-card__body">
<span class="iv-badge iv-badge--primary">Glint</span>
<h3 class="iv-card__title">A spark runs the border</h3>
<p class="iv-u-m-0">Sample content, not a real product. <code>iv-edge-glint</code> sweeps a lit segment once around the border on hover, on focus inside, and when <code>Reveal</code> marks the card as seen.</p>
</div>
<div class="iv-card__footer">
<a class="iv-button iv-button--ghost iv-button--sm" href="#effects-edges">Back to the top of the fixture</a>
</div>
</article>
<article class="iv-card iv-edge-near">
<div class="iv-card__body">
<span class="iv-badge iv-badge--info">Proximity</span>
<h3 class="iv-card__title">The edge follows the pointer</h3>
<p class="iv-u-m-0">Sample content, not a real product. <code>iv-edge-near</code> plus the <code>Proximity</code> component light the border where the pointer is, and fade it out over the last 160 pixels.</p>
</div>
<div class="iv-card__footer">
<a class="iv-button iv-button--ghost iv-button--sm" href="#effects-edges">Back to the top of the fixture</a>
</div>
</article>
<article class="iv-card iv-edge-glint iv-edge-near">
<div class="iv-card__body">
<span class="iv-badge iv-badge--success">Both</span>
<h3 class="iv-card__title">Glint and proximity together</h3>
<p class="iv-u-m-0">Sample content, not a real product. The two effects use different pseudo-elements, so one card can carry both: the glint answers the hover, the halo answers the distance.</p>
</div>
<div class="iv-card__footer">
<a class="iv-button iv-button--ghost iv-button--sm" href="#effects-edges">Back to the top of the fixture</a>
</div>
</article>
</div>
</section>Scan, sparks and pulse
iv-scan and iv-scan--v move a thin line across a large block in a slow loop; iv-spark twinkles; iv-pulse-glow breathes. Use them sparingly: one scan per hero, a handful of sparks.
<section id="effects-scan" class="iv-stack iv-u-gap-6">
<div class="iv-scan" style="min-block-size: 14rem; display: grid; align-content: end; gap: var(--iv-space-2); padding: clamp(1.5rem, 4vw, 2.5rem); border-radius: var(--iv-radius-lg); background-image: linear-gradient(140deg, var(--iv-palette-neutral-950), var(--iv-palette-green-900) 55%, var(--iv-palette-neutral-900)); color: var(--iv-palette-neutral-0); --iv-color-surface: var(--iv-palette-neutral-800)">
<p class="iv-u-m-0" style="font-family: var(--iv-font-mono); font-size: var(--iv-text-xs); letter-spacing: 0.2em; text-transform: uppercase; color: var(--iv-color-accent)">Horizontal scan</p>
<h2 class="iv-u-m-0">A line of light crosses the block</h2>
<p class="iv-u-m-0" style="max-inline-size: 52ch; color: color-mix(in srgb, var(--iv-palette-neutral-0) 84%, transparent)">Made-up copy, for the fixture only. <code>iv-scan</code> repeats every six seconds and stops under <code>prefers-reduced-motion</code>; it belongs on large blocks such as a hero, never on a paragraph.</p>
</div>
<div class="iv-scan iv-scan--v" style="min-block-size: 14rem; display: grid; align-content: end; gap: var(--iv-space-2); padding: clamp(1.5rem, 4vw, 2.5rem); border-radius: var(--iv-radius-lg); background-image: linear-gradient(200deg, var(--iv-palette-neutral-950), var(--iv-palette-blue-600) 120%); color: var(--iv-palette-neutral-0); --iv-color-surface: var(--iv-palette-neutral-800)">
<p class="iv-u-m-0" style="font-family: var(--iv-font-mono); font-size: var(--iv-text-xs); letter-spacing: 0.2em; text-transform: uppercase; color: var(--iv-color-accent)">Vertical scan</p>
<h2 class="iv-u-m-0">The same sweep, top to bottom</h2>
<p class="iv-u-m-0" style="max-inline-size: 52ch; color: color-mix(in srgb, var(--iv-palette-neutral-0) 84%, transparent)">Made-up copy, for the fixture only. <code>iv-scan--v</code> only swaps the direction and the keyframes; the duration stays in <code>--iv-scan-duration</code>.</p>
</div>
<div class="iv-cluster iv-u-gap-6" style="align-items: center; padding: var(--iv-space-6); border-radius: var(--iv-radius-lg); background-color: var(--iv-color-surface)">
<p class="iv-u-m-0"><strong>Sparks</strong>, staggered with <code>--iv-i</code>:</p>
<span class="iv-cluster iv-u-gap-4" style="align-items: center">
<span class="iv-spark" style="--iv-i: 0" aria-hidden="true"></span>
<span class="iv-spark" style="--iv-i: 1" aria-hidden="true"></span>
<span class="iv-spark" style="--iv-i: 2" aria-hidden="true"></span>
</span>
<p class="iv-u-m-0"><strong>Breathing halo</strong> for a status indicator:</p>
<span class="iv-badge iv-badge--success iv-pulse-glow">Nominal (sample)</span>
</div>
</section>.iv-root {
--iv-color-surface: var(--iv-palette-neutral-800);
--iv-i: 0;
}Reveals
Add data-iv-component="reveal" to a container and iv-reveal (with --iv-i for stagger) to its children: they fade and rise when they enter the viewport. Without JavaScript or under reduced motion they are simply visible.
<section id="effects-reveal" data-iv-component="reveal" class="iv-stack iv-u-gap-12">
<p class="iv-u-m-0">Made-up copy, for the fixture only. Scroll down: each block fades up as it enters the viewport, staggered by its inline <code>--iv-i</code>. Without JavaScript every block is visible from the start, and so it is under <code>prefers-reduced-motion</code>.</p>
<article class="iv-card iv-reveal iv-edge-glint" style="--iv-i: 0; min-block-size: 9rem">
<div class="iv-card__body">
<span class="iv-badge iv-badge--primary">01</span>
<h3 class="iv-card__title">The first block is already in view</h3>
<p class="iv-u-m-0">Entering also fires <code>iv-edge-glint</code>, so the border is drawn once as the card arrives.</p>
</div>
</article>
<article class="iv-card iv-reveal" style="--iv-i: 1; min-block-size: 9rem">
<div class="iv-card__body">
<span class="iv-badge">02</span>
<h3 class="iv-card__title">Eighty milliseconds later</h3>
<p class="iv-u-m-0">The delay is <code>--iv-i</code> multiplied by the <code>stagger</code> option, written as <code>--iv-reveal-delay</code>.</p>
</div>
</article>
<article class="iv-card iv-reveal" style="--iv-i: 2; min-block-size: 9rem">
<div class="iv-card__body">
<span class="iv-badge">03</span>
<h3 class="iv-card__title">One observer for the whole container</h3>
<p class="iv-u-m-0">The component keeps a single <code>IntersectionObserver</code>; no scroll listener, no layout read.</p>
</div>
</article>
<article class="iv-card iv-reveal" style="--iv-i: 3; min-block-size: 9rem">
<div class="iv-card__body">
<span class="iv-badge">04</span>
<h3 class="iv-card__title">Seen once, left alone</h3>
<p class="iv-u-m-0">With <code>repeat</code> off the element stops being observed as soon as it has entered.</p>
</div>
</article>
<article class="iv-card iv-reveal" style="--iv-i: 4; min-block-size: 9rem">
<div class="iv-card__body">
<span class="iv-badge">05</span>
<h3 class="iv-card__title">Threshold at fifteen per cent</h3>
<p class="iv-u-m-0">A block counts as arrived when 15 % of it is visible; the <code>threshold</code> option moves that line.</p>
</div>
</article>
<article class="iv-card iv-reveal" style="--iv-i: 5; min-block-size: 9rem">
<div class="iv-card__body">
<span class="iv-badge">06</span>
<h3 class="iv-card__title">Destroy puts the markup back</h3>
<p class="iv-u-m-0">The attribute and the delay are removed, and so is the style attribute when nothing else was using it.</p>
</div>
</article>
</section>.iv-root {
--iv-i: 0;
}Options
| Component | Option | Default | Effect |
|---|---|---|---|
Proximity | radius | 160 | Distance in pixels at which a border starts to light up. |
Reveal | threshold | 0.15 | Visible fraction that triggers the reveal. |
Reveal | repeat | false | Reveal again each time the element re-enters. |
Reveal | stagger | 80 | Milliseconds between siblings, multiplied by --iv-i. |
Common mistakes
- Glint or proximity on tiny elements: the effect reads as noise. Keep it for cards, panels and heroes.
- Relying on a reveal to show content: it is decoration; the content is in the page from the start.
- Scan lines over paragraphs: keep them on media and large surfaces.