Distortion✨ Premium

VHS Tracking

Lines of Courier text that jump sideways with a red and cyan fringe, under scanlines and a bright line rolling down: the picture of a badly tracked VHS tape.

CSSAnimationVHS

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Distortion has 31 effects, including 5 free. Explore the category →

3 usage examples

Horror film festival — PLAY and counter overlaid on the trailer — VHS Tracking example 1

① Horror film festival — PLAY and counter overlaid on the trailer

WhenOn Grimvalde's site, the trailer for "The House Under the Willows" fills the block's width; in the top-left corner, "▶ PLAY" and the SP 00:47:12 counter jump in turn while the bright line sweeps the picture.
WhyA festival that screens restored VHS copies sells the object as much as the film: the jumping overlay recalls the living-room VCR. Stretched over the whole video, the scanlines and the bright line play on the entire picture, not just on the text.
Settings--vht-width: 100%, --vht-height: 100%, --vht-panel: transparent, --vht-bg: transparent, --vht-size: 20px; root set to position: absolute; inset: 0 over the picture, text aligned left, two lines.
Family archive — the timestamp in the corner of a digitised video — VHS Tracking example 2

② Family archive — the timestamp in the corner of a digitised video

WhenIn the Brunelais family gallery, the "Léa's 6th birthday" thumbnail shows the 12.06.94 · 16:08 timestamp in its bottom-right corner, jumping every 3 s like on the original tape; the list of other tapes stays still.
WhyAt 13 px, the effect becomes a clue to the era: it dates the video without a word of explanation. A single line and a jump cut to 5 px keep the detail discreet next to the thumbnail.
Settings--vht-width: auto, --vht-size: 13px, --vht-panel: rgba(0,0,0,.28), --vht-jump: 5px, background and height released; in the scene, the screen drops its 140 px minimum height and 20 px padding (min-height: 0; padding: 3px 9px).
Podcast player — "◀◀ REW" above the bar after skipping back 15 s — VHS Tracking example 3

③ Podcast player — "◀◀ REW" above the bar after skipping back 15 s

WhenIn Cassetine, clicking "⟲ 15" moves playback back 15 s; a small "◀◀ REW" screen shows for two seconds above the playhead, then fades out. The scene replays that click in a loop.
WhySkipping back is confirmed in the language of the tape deck, in tune with the app's cassette look, with no banner or sound. Shown only during the action, the effect punctuates it instead of decorating all the time.
Settings--vht-width: auto, --vht-size: 13px, --vht-jump: 6px, background and height released, min-height: 0; padding: 5px 12px on the screen. Showing it, placing it above the playhead and moving the time back are wired in the scene.

How it works

The root holds a .vht-screen (280 px wide, #111 background, rounded corners) made of three layers: the .vht-text text, a .vht-noise pattern of 2% white lines every 6 px, and a 3 px bright .vht-line. Both decorative layers carry aria-hidden="true". Each <span> in .vht-text is one line: PLAY, SP 00:12:34 and REC by default.

Each line plays vhtTracking over 3 s. During the first 1.35 s (45 % of the cycle), it slides 8 px right (reached at 0.45 s), swings 4 px left (at 0.9 s) and settles back, while its red and cyan shadow switches sides; for the remaining 1.65 s it holds still with a 1 to 2 px fringe. The 2nd line starts 0.5 s after the 1st, the 3rd 1 s after: the glitch steps down the screen. Meanwhile, vhtLine slides the bright line from top to bottom in 4 s.

Screen width (--vht-width), text size and colour, fringe colours, jump distance (--vht-jump), cycle length and screen background are variables set on the root. The screen never overflows its container (max-width: 100%); with --vht-width: 100% and --vht-panel: transparent, it sits over an image or a video.

No JavaScript. Beyond three lines, the extra ones start together with the first: add a .vht-text span:nth-child(4) { animation-delay: 1.5s } rule to extend the staircase.

Accessibility

  • Screen readers: the text is real and read in order ("PLAY SP 00:12:34 REC"); the scanlines and the bright line, hidden with aria-hidden, are not announced.
  • Reduced motion: under prefers-reduced-motion: reduce, the lines stop with their 2 px fringe, the scanlines stay and the bright line is removed. Before the repair, all four animations kept running.
  • Flashing: the bright line is at 15% opacity and the glitch only takes up 1.35 s of each 3 s cycle; frame by frame, no cell goes above 3 changes per second.
  • Contrast: white text on #111 by default. Over a video with --vht-panel: transparent, make sure the picture behind stays dark enough, or keep a semi-opaque background (rgba(0,0,0,.35)).
  • Meaning: PLAY or REC are labels, not controls; in a real player, the controls remain real <button> elements outside the effect.

Browser compatibility

Pure CSS (2.9 KB), no dependency: CSS variables inside keyframes (transform, text-shadow), a repeating gradient for the scanlines, an animated top for the bright line. Measured in Chromium 151, Firefox 153 and WebKit 26.5: same look as the previous version once the lines are running (only difference: lines 2 and 3 already show their fringe during their start delay), two independent copies, no overflow at 390 px, 0 errors.

Chrome 49+✓ Full (measured)
Firefox 31+✓ Full (measured)
Safari 9.1+✓ Full (WebKit engine measured)
Edge 79+✓ Full (Chromium engine)
Mobile iOS 9.3+✓ Full (WebKit engine measured)
Android Chrome 49+✓ Full (Chromium engine)

Without CSS animations, the screen stays frozen: text with its fringe, scanlines visible, bright line hidden above the edge. Without CSS variables, the adjustable colours and width no longer apply and the text takes the page's style.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0272-root">
  <div class="vht-screen">
    <div class="vht-text">
      <span>PLAY</span>
      <span>SP 00:12:34</span>
      <span>REC</span>
    </div>
    <div class="vht-noise" aria-hidden="true"></div>
    <div class="vht-line" aria-hidden="true"></div>
  </div>
</div>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Lines (HTML — one <span> per line in .vht-text) PLAY · SP 00:12:34 · REC One to three lines each get their own offset; a single line is enough for a timestamp.
--vht-width 280px Screen width; 100% to cover a picture, auto to fit the text.
--vht-size · --vht-color 18px · #fff Text size and colour.
--vht-red · --vht-cyan red · #0ff Colours of the two fringes around the letters.
--vht-jump 8px Largest jump to the right; the swing back left is half of it. 4 to 5 px for small text.
--vht-speed 3s Length of one tracking cycle; 6s makes the glitch rarer.
--vht-panel #111 Screen background; transparent or semi-opaque for an overlay.
--vht-bg · --vht-height #0a0a0f · 300px Root background and minimum height; transparent and auto to blend it in.

FAQ

Yes: place the root with position: absolute; inset: 0 inside the <video> container, with --vht-width: 100%, --vht-height: 100% and --vht-panel: transparent. Add pointer-events: none to the root so clicks reach the video controls.
It only exists to travel across the screen; standing still, it would look like a stray stroke. The text keeps its fringe and the scanlines stay: the tape look remains with nothing moving.
The red and cyan fringes stand out on dark; on a light background they read as a printing fault. For a light page, keep a small dark screen (--vht-panel) around the text, like the timestamp in the examples.