25 CSS Background Animations12 / 25
CRT Scanline Overlay Background
A CRT that actually behaves like one: an aperture-grille RGB triad at sub-pixel scale, horizontal scanlines at a different pitch, a rolling refresh bar that drifts down the tube, phosphor bloom, barrel-vignette curvature, and a chromatic-aberration text shadow. Every layer is a gradient — no images, no canvas, no JavaScript.
Published
The code
<section class="bga-12" aria-label="CRT scanline retro monitor background demo">
<div class="bga-12__phosphor" aria-hidden="true"></div>
<div class="bga-12__grille" aria-hidden="true"></div>
<div class="bga-12__scan" aria-hidden="true"></div>
<div class="bga-12__roll" aria-hidden="true"></div>
<div class="bga-12__glass" aria-hidden="true"></div>
<div class="bga-12__stage">
<div class="bga-12__term">
<p class="bga-12__line bga-12__line--dim">CODEFRONTS CRT EMULATOR v2.7 — 640×480 @ 60Hz</p>
<h1 class="bga-12__title">READY.</h1>
<p class="bga-12__body">Aperture grille at 3px. Scanlines at 4px. Coprime pitches, so the two grids never resolve into a checkerboard.</p>
<ul class="bga-12__list">
<li><span>MASK</span>RGB triad · repeating-linear-gradient 90deg</li>
<li><span>SCAN</span>2px on / 2px off · 34% black</li>
<li><span>ROLL</span>translate 9s linear · vertical hold drift</li>
<li><span>CONV</span>±1px red / cyan text-shadow</li>
</ul>
<p class="bga-12__prompt">> <span class="bga-12__caret"></span></p>
</div>
</div>
</section><section class="bga-12" aria-label="CRT scanline retro monitor background demo">
<div class="bga-12__phosphor" aria-hidden="true"></div>
<div class="bga-12__grille" aria-hidden="true"></div>
<div class="bga-12__scan" aria-hidden="true"></div>
<div class="bga-12__roll" aria-hidden="true"></div>
<div class="bga-12__glass" aria-hidden="true"></div>
<div class="bga-12__stage">
<div class="bga-12__term">
<p class="bga-12__line bga-12__line--dim">CODEFRONTS CRT EMULATOR v2.7 — 640×480 @ 60Hz</p>
<h1 class="bga-12__title">READY.</h1>
<p class="bga-12__body">Aperture grille at 3px. Scanlines at 4px. Coprime pitches, so the two grids never resolve into a checkerboard.</p>
<ul class="bga-12__list">
<li><span>MASK</span>RGB triad · repeating-linear-gradient 90deg</li>
<li><span>SCAN</span>2px on / 2px off · 34% black</li>
<li><span>ROLL</span>translate 9s linear · vertical hold drift</li>
<li><span>CONV</span>±1px red / cyan text-shadow</li>
</ul>
<p class="bga-12__prompt">> <span class="bga-12__caret"></span></p>
</div>
</div>
</section>.bga-12,
.bga-12 *,
.bga-12 *::before,
.bga-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-12 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-12-ph: oklch(0.86 0.19 148);
--bga-12-bg: oklch(0.16 0.035 155);
background: oklch(0.09 0.02 155);
color: var(--bga-12-ph);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
-webkit-font-smoothing: antialiased;
}
.bga-12__phosphor {
position: absolute;
inset: 0;
background: radial-gradient(100% 82% at 50% 46%,var(--bga-12-bg),oklch(0.08 0.015 155) 78%);
animation: bga-12-hum 5.2s steps(6) infinite;
}
@keyframes bga-12-hum {
0%,
100% {
filter: brightness(.96);
}
50% {
filter: brightness(1.12);
}
}
.bga-12__grille {
position: absolute;
inset: 0;
pointer-events: none;
mix-blend-mode: overlay;
opacity: .85;
background: repeating-linear-gradient(90deg,
oklch(0.62 0.22 25/.2) 0 1px,oklch(0.75 0.22 145/.2) 1px 2px,oklch(0.6 0.22 265/.2) 2px 3px);
}
.bga-12__scan {
position: absolute;
inset: -4px 0 0;
pointer-events: none;
background: repeating-linear-gradient(oklch(0 0 0/.4) 0 2px,transparent 2px 4px);
animation: bga-12-scan 2.6s linear infinite;
}
@keyframes bga-12-scan {
to {
background-position: 0 4px;
}
}
.bga-12__roll {
position: absolute;
inset-inline: 0;
top: -34%;
height: 30%;
pointer-events: none;
mix-blend-mode: screen;
opacity: .62;
background: linear-gradient(oklch(1 0 0/0) 0 6%,oklch(0.9 0.06 150/.3) 44%,oklch(0.97 0.05 150/.5) 84%,oklch(1 0 0/.34) 96%,oklch(1 0 0/0));
animation: bga-12-roll 6.5s linear infinite;
}
@keyframes bga-12-roll {
to {
translate: 0 460%;
}
}
.bga-12__glass {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(60% 40% at 26% 6%,oklch(1 0 0/.07),transparent 62%), radial-gradient(132% 112% at 50% 50%,transparent 52%,oklch(0 0 0/.86) 100%);
}
.bga-12__stage {
position: relative;
z-index: 4;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,70px);
}
.bga-12__term {
width: min(100%,720px);
display: grid;
gap: 14px;
animation: bga-12-track 7.4s steps(1) infinite;
text-shadow: 1px 0 0 oklch(0.6 0.22 25/.55),-1px 0 0 oklch(0.7 0.16 210/.55),0 0 18px color-mix(in oklab,var(--bga-12-ph) 55%,transparent);
}
.bga-12__line {
font-size: clamp(10.5px,1.1vw,12.5px);
letter-spacing: .12em;
text-transform: uppercase;
}
.bga-12__line--dim {
color: color-mix(in oklab,var(--bga-12-ph) 58%,transparent);
}
.bga-12__title {
font-size: clamp(34px,8vw,86px);
font-weight: 700;
letter-spacing: .02em;
line-height: 1;
}
.bga-12__body {
max-width: 56ch;
font-size: clamp(12.5px,1.25vw,15px);
line-height: 1.65;
color: color-mix(in oklab,var(--bga-12-ph) 82%,transparent);
text-wrap: pretty;
}
.bga-12__list {
list-style: none;
display: grid;
gap: 6px;
margin-top: 6px;
padding: 16px;
border: 1px solid color-mix(in oklab,var(--bga-12-ph) 26%,transparent);
background: oklch(0.12 0.03 155/.5);
}
.bga-12__list li {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: clamp(11px,1.1vw,13px);
color: color-mix(in oklab,var(--bga-12-ph) 78%,transparent);
}
.bga-12__list span {
min-width: 56px;
font-weight: 700;
color: var(--bga-12-ph);
}
.bga-12__prompt {
display: flex;
align-items: center;
font-size: clamp(14px,1.6vw,20px);
}
.bga-12__caret {
display: inline-block;
width: .62em;
height: 1.05em;
background: var(--bga-12-ph);
animation: bga-12-blink 1.06s steps(1) infinite;
}
@keyframes bga-12-blink {
50% {
opacity: 0;
}
}
@keyframes bga-12-track {
0%,
42% {
translate: 0 0;
}
43% {
translate: -3px 1px;
}
44%,
86% {
translate: 0 0;
}
87% {
translate: 2px -1px;
}
88%,
100% {
translate: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.bga-12__phosphor,
.bga-12__roll,
.bga-12__caret,
.bga-12__scan,
.bga-12__term {
animation: none;
}
.bga-12__roll {
opacity: .18;
top: 22%;
}
}.bga-12,
.bga-12 *,
.bga-12 *::before,
.bga-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-12 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-12-ph: oklch(0.86 0.19 148);
--bga-12-bg: oklch(0.16 0.035 155);
background: oklch(0.09 0.02 155);
color: var(--bga-12-ph);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
-webkit-font-smoothing: antialiased;
}
.bga-12__phosphor {
position: absolute;
inset: 0;
background: radial-gradient(100% 82% at 50% 46%,var(--bga-12-bg),oklch(0.08 0.015 155) 78%);
animation: bga-12-hum 5.2s steps(6) infinite;
}
@keyframes bga-12-hum {
0%,
100% {
filter: brightness(.96);
}
50% {
filter: brightness(1.12);
}
}
.bga-12__grille {
position: absolute;
inset: 0;
pointer-events: none;
mix-blend-mode: overlay;
opacity: .85;
background: repeating-linear-gradient(90deg,
oklch(0.62 0.22 25/.2) 0 1px,oklch(0.75 0.22 145/.2) 1px 2px,oklch(0.6 0.22 265/.2) 2px 3px);
}
.bga-12__scan {
position: absolute;
inset: -4px 0 0;
pointer-events: none;
background: repeating-linear-gradient(oklch(0 0 0/.4) 0 2px,transparent 2px 4px);
animation: bga-12-scan 2.6s linear infinite;
}
@keyframes bga-12-scan {
to {
background-position: 0 4px;
}
}
.bga-12__roll {
position: absolute;
inset-inline: 0;
top: -34%;
height: 30%;
pointer-events: none;
mix-blend-mode: screen;
opacity: .62;
background: linear-gradient(oklch(1 0 0/0) 0 6%,oklch(0.9 0.06 150/.3) 44%,oklch(0.97 0.05 150/.5) 84%,oklch(1 0 0/.34) 96%,oklch(1 0 0/0));
animation: bga-12-roll 6.5s linear infinite;
}
@keyframes bga-12-roll {
to {
translate: 0 460%;
}
}
.bga-12__glass {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(60% 40% at 26% 6%,oklch(1 0 0/.07),transparent 62%), radial-gradient(132% 112% at 50% 50%,transparent 52%,oklch(0 0 0/.86) 100%);
}
.bga-12__stage {
position: relative;
z-index: 4;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,70px);
}
.bga-12__term {
width: min(100%,720px);
display: grid;
gap: 14px;
animation: bga-12-track 7.4s steps(1) infinite;
text-shadow: 1px 0 0 oklch(0.6 0.22 25/.55),-1px 0 0 oklch(0.7 0.16 210/.55),0 0 18px color-mix(in oklab,var(--bga-12-ph) 55%,transparent);
}
.bga-12__line {
font-size: clamp(10.5px,1.1vw,12.5px);
letter-spacing: .12em;
text-transform: uppercase;
}
.bga-12__line--dim {
color: color-mix(in oklab,var(--bga-12-ph) 58%,transparent);
}
.bga-12__title {
font-size: clamp(34px,8vw,86px);
font-weight: 700;
letter-spacing: .02em;
line-height: 1;
}
.bga-12__body {
max-width: 56ch;
font-size: clamp(12.5px,1.25vw,15px);
line-height: 1.65;
color: color-mix(in oklab,var(--bga-12-ph) 82%,transparent);
text-wrap: pretty;
}
.bga-12__list {
list-style: none;
display: grid;
gap: 6px;
margin-top: 6px;
padding: 16px;
border: 1px solid color-mix(in oklab,var(--bga-12-ph) 26%,transparent);
background: oklch(0.12 0.03 155/.5);
}
.bga-12__list li {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: clamp(11px,1.1vw,13px);
color: color-mix(in oklab,var(--bga-12-ph) 78%,transparent);
}
.bga-12__list span {
min-width: 56px;
font-weight: 700;
color: var(--bga-12-ph);
}
.bga-12__prompt {
display: flex;
align-items: center;
font-size: clamp(14px,1.6vw,20px);
}
.bga-12__caret {
display: inline-block;
width: .62em;
height: 1.05em;
background: var(--bga-12-ph);
animation: bga-12-blink 1.06s steps(1) infinite;
}
@keyframes bga-12-blink {
50% {
opacity: 0;
}
}
@keyframes bga-12-track {
0%,
42% {
translate: 0 0;
}
43% {
translate: -3px 1px;
}
44%,
86% {
translate: 0 0;
}
87% {
translate: 2px -1px;
}
88%,
100% {
translate: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.bga-12__phosphor,
.bga-12__roll,
.bga-12__caret,
.bga-12__scan,
.bga-12__term {
animation: none;
}
.bga-12__roll {
opacity: .18;
top: 22%;
}
}Here's a working CSS Background Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CRT Scanline Overlay Background
Source: https://codefronts.com/motion/css-background-animations/crt-scanline-overlay-background/
A CRT that actually behaves like one: an aperture-grille RGB triad at sub-pixel scale, horizontal scanlines at a different pitch, a rolling refresh bar that drifts down the tube, phosphor bloom, barrel-vignette curvature, and a chromatic-aberration text shadow. Every layer is a gradient — no images, no canvas, no JavaScript.
## HTML
```html
<section class="bga-12" aria-label="CRT scanline retro monitor background demo">
<div class="bga-12__phosphor" aria-hidden="true"></div>
<div class="bga-12__grille" aria-hidden="true"></div>
<div class="bga-12__scan" aria-hidden="true"></div>
<div class="bga-12__roll" aria-hidden="true"></div>
<div class="bga-12__glass" aria-hidden="true"></div>
<div class="bga-12__stage">
<div class="bga-12__term">
<p class="bga-12__line bga-12__line--dim">CODEFRONTS CRT EMULATOR v2.7 — 640×480 @ 60Hz</p>
<h1 class="bga-12__title">READY.</h1>
<p class="bga-12__body">Aperture grille at 3px. Scanlines at 4px. Coprime pitches, so the two grids never resolve into a checkerboard.</p>
<ul class="bga-12__list">
<li><span>MASK</span>RGB triad · repeating-linear-gradient 90deg</li>
<li><span>SCAN</span>2px on / 2px off · 34% black</li>
<li><span>ROLL</span>translate 9s linear · vertical hold drift</li>
<li><span>CONV</span>±1px red / cyan text-shadow</li>
</ul>
<p class="bga-12__prompt">> <span class="bga-12__caret"></span></p>
</div>
</div>
</section>
```
## CSS
```css
.bga-12,
.bga-12 *,
.bga-12 *::before,
.bga-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-12 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-12-ph: oklch(0.86 0.19 148);
--bga-12-bg: oklch(0.16 0.035 155);
background: oklch(0.09 0.02 155);
color: var(--bga-12-ph);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
-webkit-font-smoothing: antialiased;
}
.bga-12__phosphor {
position: absolute;
inset: 0;
background: radial-gradient(100% 82% at 50% 46%,var(--bga-12-bg),oklch(0.08 0.015 155) 78%);
animation: bga-12-hum 5.2s steps(6) infinite;
}
@keyframes bga-12-hum {
0%,
100% {
filter: brightness(.96);
}
50% {
filter: brightness(1.12);
}
}
.bga-12__grille {
position: absolute;
inset: 0;
pointer-events: none;
mix-blend-mode: overlay;
opacity: .85;
background: repeating-linear-gradient(90deg,
oklch(0.62 0.22 25/.2) 0 1px,oklch(0.75 0.22 145/.2) 1px 2px,oklch(0.6 0.22 265/.2) 2px 3px);
}
.bga-12__scan {
position: absolute;
inset: -4px 0 0;
pointer-events: none;
background: repeating-linear-gradient(oklch(0 0 0/.4) 0 2px,transparent 2px 4px);
animation: bga-12-scan 2.6s linear infinite;
}
@keyframes bga-12-scan {
to {
background-position: 0 4px;
}
}
.bga-12__roll {
position: absolute;
inset-inline: 0;
top: -34%;
height: 30%;
pointer-events: none;
mix-blend-mode: screen;
opacity: .62;
background: linear-gradient(oklch(1 0 0/0) 0 6%,oklch(0.9 0.06 150/.3) 44%,oklch(0.97 0.05 150/.5) 84%,oklch(1 0 0/.34) 96%,oklch(1 0 0/0));
animation: bga-12-roll 6.5s linear infinite;
}
@keyframes bga-12-roll {
to {
translate: 0 460%;
}
}
.bga-12__glass {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(60% 40% at 26% 6%,oklch(1 0 0/.07),transparent 62%), radial-gradient(132% 112% at 50% 50%,transparent 52%,oklch(0 0 0/.86) 100%);
}
.bga-12__stage {
position: relative;
z-index: 4;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,70px);
}
.bga-12__term {
width: min(100%,720px);
display: grid;
gap: 14px;
animation: bga-12-track 7.4s steps(1) infinite;
text-shadow: 1px 0 0 oklch(0.6 0.22 25/.55),-1px 0 0 oklch(0.7 0.16 210/.55),0 0 18px color-mix(in oklab,var(--bga-12-ph) 55%,transparent);
}
.bga-12__line {
font-size: clamp(10.5px,1.1vw,12.5px);
letter-spacing: .12em;
text-transform: uppercase;
}
.bga-12__line--dim {
color: color-mix(in oklab,var(--bga-12-ph) 58%,transparent);
}
.bga-12__title {
font-size: clamp(34px,8vw,86px);
font-weight: 700;
letter-spacing: .02em;
line-height: 1;
}
.bga-12__body {
max-width: 56ch;
font-size: clamp(12.5px,1.25vw,15px);
line-height: 1.65;
color: color-mix(in oklab,var(--bga-12-ph) 82%,transparent);
text-wrap: pretty;
}
.bga-12__list {
list-style: none;
display: grid;
gap: 6px;
margin-top: 6px;
padding: 16px;
border: 1px solid color-mix(in oklab,var(--bga-12-ph) 26%,transparent);
background: oklch(0.12 0.03 155/.5);
}
.bga-12__list li {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: clamp(11px,1.1vw,13px);
color: color-mix(in oklab,var(--bga-12-ph) 78%,transparent);
}
.bga-12__list span {
min-width: 56px;
font-weight: 700;
color: var(--bga-12-ph);
}
.bga-12__prompt {
display: flex;
align-items: center;
font-size: clamp(14px,1.6vw,20px);
}
.bga-12__caret {
display: inline-block;
width: .62em;
height: 1.05em;
background: var(--bga-12-ph);
animation: bga-12-blink 1.06s steps(1) infinite;
}
@keyframes bga-12-blink {
50% {
opacity: 0;
}
}
@keyframes bga-12-track {
0%,
42% {
translate: 0 0;
}
43% {
translate: -3px 1px;
}
44%,
86% {
translate: 0 0;
}
87% {
translate: 2px -1px;
}
88%,
100% {
translate: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.bga-12__phosphor,
.bga-12__roll,
.bga-12__caret,
.bga-12__scan,
.bga-12__term {
animation: none;
}
.bga-12__roll {
opacity: .18;
top: 22%;
}
}
```Here's a working CSS Background Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CRT Scanline Overlay Background
Source: https://codefronts.com/motion/css-background-animations/crt-scanline-overlay-background/
A CRT that actually behaves like one: an aperture-grille RGB triad at sub-pixel scale, horizontal scanlines at a different pitch, a rolling refresh bar that drifts down the tube, phosphor bloom, barrel-vignette curvature, and a chromatic-aberration text shadow. Every layer is a gradient — no images, no canvas, no JavaScript.
## HTML
```html
<section class="bga-12" aria-label="CRT scanline retro monitor background demo">
<div class="bga-12__phosphor" aria-hidden="true"></div>
<div class="bga-12__grille" aria-hidden="true"></div>
<div class="bga-12__scan" aria-hidden="true"></div>
<div class="bga-12__roll" aria-hidden="true"></div>
<div class="bga-12__glass" aria-hidden="true"></div>
<div class="bga-12__stage">
<div class="bga-12__term">
<p class="bga-12__line bga-12__line--dim">CODEFRONTS CRT EMULATOR v2.7 — 640×480 @ 60Hz</p>
<h1 class="bga-12__title">READY.</h1>
<p class="bga-12__body">Aperture grille at 3px. Scanlines at 4px. Coprime pitches, so the two grids never resolve into a checkerboard.</p>
<ul class="bga-12__list">
<li><span>MASK</span>RGB triad · repeating-linear-gradient 90deg</li>
<li><span>SCAN</span>2px on / 2px off · 34% black</li>
<li><span>ROLL</span>translate 9s linear · vertical hold drift</li>
<li><span>CONV</span>±1px red / cyan text-shadow</li>
</ul>
<p class="bga-12__prompt">> <span class="bga-12__caret"></span></p>
</div>
</div>
</section>
```
## CSS
```css
.bga-12,
.bga-12 *,
.bga-12 *::before,
.bga-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-12 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-12-ph: oklch(0.86 0.19 148);
--bga-12-bg: oklch(0.16 0.035 155);
background: oklch(0.09 0.02 155);
color: var(--bga-12-ph);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
-webkit-font-smoothing: antialiased;
}
.bga-12__phosphor {
position: absolute;
inset: 0;
background: radial-gradient(100% 82% at 50% 46%,var(--bga-12-bg),oklch(0.08 0.015 155) 78%);
animation: bga-12-hum 5.2s steps(6) infinite;
}
@keyframes bga-12-hum {
0%,
100% {
filter: brightness(.96);
}
50% {
filter: brightness(1.12);
}
}
.bga-12__grille {
position: absolute;
inset: 0;
pointer-events: none;
mix-blend-mode: overlay;
opacity: .85;
background: repeating-linear-gradient(90deg,
oklch(0.62 0.22 25/.2) 0 1px,oklch(0.75 0.22 145/.2) 1px 2px,oklch(0.6 0.22 265/.2) 2px 3px);
}
.bga-12__scan {
position: absolute;
inset: -4px 0 0;
pointer-events: none;
background: repeating-linear-gradient(oklch(0 0 0/.4) 0 2px,transparent 2px 4px);
animation: bga-12-scan 2.6s linear infinite;
}
@keyframes bga-12-scan {
to {
background-position: 0 4px;
}
}
.bga-12__roll {
position: absolute;
inset-inline: 0;
top: -34%;
height: 30%;
pointer-events: none;
mix-blend-mode: screen;
opacity: .62;
background: linear-gradient(oklch(1 0 0/0) 0 6%,oklch(0.9 0.06 150/.3) 44%,oklch(0.97 0.05 150/.5) 84%,oklch(1 0 0/.34) 96%,oklch(1 0 0/0));
animation: bga-12-roll 6.5s linear infinite;
}
@keyframes bga-12-roll {
to {
translate: 0 460%;
}
}
.bga-12__glass {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(60% 40% at 26% 6%,oklch(1 0 0/.07),transparent 62%), radial-gradient(132% 112% at 50% 50%,transparent 52%,oklch(0 0 0/.86) 100%);
}
.bga-12__stage {
position: relative;
z-index: 4;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,70px);
}
.bga-12__term {
width: min(100%,720px);
display: grid;
gap: 14px;
animation: bga-12-track 7.4s steps(1) infinite;
text-shadow: 1px 0 0 oklch(0.6 0.22 25/.55),-1px 0 0 oklch(0.7 0.16 210/.55),0 0 18px color-mix(in oklab,var(--bga-12-ph) 55%,transparent);
}
.bga-12__line {
font-size: clamp(10.5px,1.1vw,12.5px);
letter-spacing: .12em;
text-transform: uppercase;
}
.bga-12__line--dim {
color: color-mix(in oklab,var(--bga-12-ph) 58%,transparent);
}
.bga-12__title {
font-size: clamp(34px,8vw,86px);
font-weight: 700;
letter-spacing: .02em;
line-height: 1;
}
.bga-12__body {
max-width: 56ch;
font-size: clamp(12.5px,1.25vw,15px);
line-height: 1.65;
color: color-mix(in oklab,var(--bga-12-ph) 82%,transparent);
text-wrap: pretty;
}
.bga-12__list {
list-style: none;
display: grid;
gap: 6px;
margin-top: 6px;
padding: 16px;
border: 1px solid color-mix(in oklab,var(--bga-12-ph) 26%,transparent);
background: oklch(0.12 0.03 155/.5);
}
.bga-12__list li {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: clamp(11px,1.1vw,13px);
color: color-mix(in oklab,var(--bga-12-ph) 78%,transparent);
}
.bga-12__list span {
min-width: 56px;
font-weight: 700;
color: var(--bga-12-ph);
}
.bga-12__prompt {
display: flex;
align-items: center;
font-size: clamp(14px,1.6vw,20px);
}
.bga-12__caret {
display: inline-block;
width: .62em;
height: 1.05em;
background: var(--bga-12-ph);
animation: bga-12-blink 1.06s steps(1) infinite;
}
@keyframes bga-12-blink {
50% {
opacity: 0;
}
}
@keyframes bga-12-track {
0%,
42% {
translate: 0 0;
}
43% {
translate: -3px 1px;
}
44%,
86% {
translate: 0 0;
}
87% {
translate: 2px -1px;
}
88%,
100% {
translate: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.bga-12__phosphor,
.bga-12__roll,
.bga-12__caret,
.bga-12__scan,
.bga-12__term {
animation: none;
}
.bga-12__roll {
opacity: .18;
top: 22%;
}
}
```How this works
Four independent gradient layers stack to make a tube. First, the shadow mask — vertical RGB stripes at 3px, the width of one triad:
background: repeating-linear-gradient(90deg,
oklch(.6 .22 25/.16) 0 1px,
oklch(.7 .22 145/.16) 1px 2px,
oklch(.6 .22 265/.16) 2px 3px);Second, scanlines — horizontal dark bands at 4px, deliberately not a multiple of 3, so the two grids never align into a visible checker:
repeating-linear-gradient(oklch(0 0 0/.34) 0 2px, transparent 2px 4px)Those scanlines also crawl. Shifting background-position by exactly one 4px period makes the raster drift upward forever with no seam — the barely-there vertical creep every real tube has:
@keyframes scan { to { background-position: 0 4px; } }Third, the roll bar: a tall gradient band with a bright leading edge, translated down the screen on a 6.5s period — the vertical-hold drift of an out-of-sync tube. It is a translate animation on a single element, so it is compositor-only.
Fourth, curvature. Real barrel distortion needs a shader, but the perceptual cues are a radial vignette plus a bright specular sheen in the upper corners — and those are two more gradients:
background: radial-gradient(130% 110% at 50% 50%, transparent 55%, #000c 100%);Text sells it: a two-colour text-shadow offset ±1px in red and cyan is convergence error, a 6-step brightness flicker is the mains hum, and a steps(1) keyframe that nudges the whole terminal 3px sideways twice per cycle is horizontal tracking error — two frames of misalignment every 7.4 seconds, which the eye reads as a tape head losing lock. Both stop under prefers-reduced-motion, which matters more here than in any other demo on this page.
Make it yours
- Change the phosphor by recolouring the text and glow: P1 green (oklch .85 .2 145), amber (oklch .8 .17 75) or white P4 for a broadcast monitor.
- Tighten scanline pitch to 3px for a 15kHz arcade look, or loosen to 6px for a large 1980s console monitor.
- Add
filter: blur(.4px) saturate(1.3)on the content layer for authentic phosphor bloom around bright text. - Skip the roll bar and the tracking glitch for a well-adjusted set; slow the roll to 14s for a tube that merely needs its vertical hold nudged, or speed the scan drift to 1.2s for a badly synced one.
Gotchas — read before shipping
- Fast flicker is a seizure risk. Keep any luminance oscillation under 3Hz and shallow, and disable it entirely under
prefers-reduced-motion— this is a genuine WCAG 2.3.1 concern. - Scanline pitch that shares a factor with the RGB stripe pitch creates a static moiré checkerboard. Use coprime values (3px and 4px here).
- 1px gradient stops land on fractional device pixels at non-integer zoom, producing shimmer. Accept it, or snap the overlay to
image-rendering: pixelatedon a scaled tile. - Chromatic-aberration text-shadow costs legibility. Keep it at 1px, never on body copy below 14px.
- Stacked full-screen blended overlays are fill-rate heavy on mobile GPUs — this demo caps at four.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome any modern.
Repeating gradients, mix-blend-mode, text-shadow and keyframes only — supported universally since 2016. oklch() raises the practical floor to 2023 engines and can be swapped for rgba() with no structural change.