16 CSS Glitch Text Effects13 / 16
Glitch Text with Noise Overlay
Animated pixel noise generated every frame by a canvas element fills the background, while the overlaid headline uses mix-blend-mode:screen to cut through the grain with a warm orange channel-split glitch that erupts periodically.
Published
The code
<div class="gt-13">
<canvas class="gt-13__noise" id="gt-13-noise"></canvas>
<div class="gt-13__stage">
<div class="gt-13__word" data-text="FRACTAL">FRACTAL</div>
<div class="gt-13__sub">noise · signal · entropy</div>
</div>
</div><div class="gt-13">
<canvas class="gt-13__noise" id="gt-13-noise"></canvas>
<div class="gt-13__stage">
<div class="gt-13__word" data-text="FRACTAL">FRACTAL</div>
<div class="gt-13__sub">noise · signal · entropy</div>
</div>
</div>.gt-13,
.gt-13 *,
.gt-13 *::before,
.gt-13 *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.gt-13 ::selection {
background: #f97316;
color: #000;
}
.gt-13 {
--orange: #f97316;
--yellow: #fbbf24;
--bg: #09060a;
--text: #fff9f0;
min-height: 100vh;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 48px 20px;
font-family: 'Inter', system-ui, sans-serif;
position: relative;
overflow: hidden;
}
/* Noise canvas fills the bg. opacity bumped 0.18 -> 0.35 so the
warm-grain texture is clearly visible behind the title even in
static thumbnails; the live demo still feels atmospheric, the
gallery card now actually advertises the noise overlay effect. */
.gt-13__noise {
position: absolute;
inset: 0;
opacity: 0.35;
pointer-events: none;
z-index: 0;
}
.gt-13__stage {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
/* Main word cuts through noise. Font scaled up clamp min 64px (was
48px) and max 120px (was 96px) so the title fills more of the
capture frame. Permanent dual-shadow (orange offset left, yellow
offset right) gives a baseline chromatic split so the gallery
thumbnail reads as a glitch even when caught between the slice
keyframes; the animated slice pseudos still layer on top during
live playback. */
.gt-13__word {
position: relative;
font-size: clamp(64px, 12vw, 120px);
font-weight: 900;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--text);
/* Text cuts through noise via mix-blend-mode */
mix-blend-mode: screen;
text-shadow: -3px 0 0 rgba(249,115,22,0.85), 3px 0 0 rgba(251,191,36,0.85), 0 0 20px rgba(249,115,22,0.4);
animation: gt-13-main-glitch 4s steps(1) infinite;
}
.gt-13__word::before {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--orange);
mix-blend-mode: screen;
clip-path: polygon(0 15%, 100% 15%, 100% 38%, 0 38%);
animation: gt-13-slice-a 4s steps(1) infinite;
}
.gt-13__word::after {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--yellow);
mix-blend-mode: screen;
clip-path: polygon(0 62%, 100% 62%, 100% 85%, 0 85%);
animation: gt-13-slice-b 4s steps(1) infinite;
}
@keyframes gt-13-main-glitch {
0%,
80%,
100% {
transform: translateX(0);
}
82% {
transform: translateX(-6px) skewX(-2deg);
}
84% {
transform: translateX(5px);
}
86% {
transform: translateX(-2px) skewX(1deg);
}
88% {
transform: translateX(0);
}
}
@keyframes gt-13-slice-a {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(-14px);
clip-path: polygon(0 10%, 100% 10%, 100% 35%, 0 35%);
}
84% {
opacity: 1;
transform: translateX(10px);
clip-path: polygon(0 18%, 100% 18%, 100% 40%, 0 40%);
}
86% {
opacity: 0.6;
transform: translateX(-4px);
}
88% {
opacity: 0;
}
}
@keyframes gt-13-slice-b {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(16px);
clip-path: polygon(0 65%, 100% 65%, 100% 88%, 0 88%);
}
84% {
opacity: 1;
transform: translateX(-10px);
clip-path: polygon(0 58%, 100% 58%, 100% 82%, 0 82%);
}
86% {
opacity: 0.6;
transform: translateX(5px);
}
88% {
opacity: 0;
}
}
.gt-13__sub {
font-size: 12px;
letter-spacing: 5px;
color: rgba(249,115,22,0.5);
text-transform: uppercase;
mix-blend-mode: screen;
}
@media (prefers-reduced-motion: reduce) {
.gt-13__word,
.gt-13__word::before,
.gt-13__word::after {
animation: none;
}
.gt-13__word::before,
.gt-13__word::after {
opacity: 0;
}
.gt-13__noise {
animation: none;
}
}.gt-13,
.gt-13 *,
.gt-13 *::before,
.gt-13 *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.gt-13 ::selection {
background: #f97316;
color: #000;
}
.gt-13 {
--orange: #f97316;
--yellow: #fbbf24;
--bg: #09060a;
--text: #fff9f0;
min-height: 100vh;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 48px 20px;
font-family: 'Inter', system-ui, sans-serif;
position: relative;
overflow: hidden;
}
/* Noise canvas fills the bg. opacity bumped 0.18 -> 0.35 so the
warm-grain texture is clearly visible behind the title even in
static thumbnails; the live demo still feels atmospheric, the
gallery card now actually advertises the noise overlay effect. */
.gt-13__noise {
position: absolute;
inset: 0;
opacity: 0.35;
pointer-events: none;
z-index: 0;
}
.gt-13__stage {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
/* Main word cuts through noise. Font scaled up clamp min 64px (was
48px) and max 120px (was 96px) so the title fills more of the
capture frame. Permanent dual-shadow (orange offset left, yellow
offset right) gives a baseline chromatic split so the gallery
thumbnail reads as a glitch even when caught between the slice
keyframes; the animated slice pseudos still layer on top during
live playback. */
.gt-13__word {
position: relative;
font-size: clamp(64px, 12vw, 120px);
font-weight: 900;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--text);
/* Text cuts through noise via mix-blend-mode */
mix-blend-mode: screen;
text-shadow: -3px 0 0 rgba(249,115,22,0.85), 3px 0 0 rgba(251,191,36,0.85), 0 0 20px rgba(249,115,22,0.4);
animation: gt-13-main-glitch 4s steps(1) infinite;
}
.gt-13__word::before {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--orange);
mix-blend-mode: screen;
clip-path: polygon(0 15%, 100% 15%, 100% 38%, 0 38%);
animation: gt-13-slice-a 4s steps(1) infinite;
}
.gt-13__word::after {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--yellow);
mix-blend-mode: screen;
clip-path: polygon(0 62%, 100% 62%, 100% 85%, 0 85%);
animation: gt-13-slice-b 4s steps(1) infinite;
}
@keyframes gt-13-main-glitch {
0%,
80%,
100% {
transform: translateX(0);
}
82% {
transform: translateX(-6px) skewX(-2deg);
}
84% {
transform: translateX(5px);
}
86% {
transform: translateX(-2px) skewX(1deg);
}
88% {
transform: translateX(0);
}
}
@keyframes gt-13-slice-a {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(-14px);
clip-path: polygon(0 10%, 100% 10%, 100% 35%, 0 35%);
}
84% {
opacity: 1;
transform: translateX(10px);
clip-path: polygon(0 18%, 100% 18%, 100% 40%, 0 40%);
}
86% {
opacity: 0.6;
transform: translateX(-4px);
}
88% {
opacity: 0;
}
}
@keyframes gt-13-slice-b {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(16px);
clip-path: polygon(0 65%, 100% 65%, 100% 88%, 0 88%);
}
84% {
opacity: 1;
transform: translateX(-10px);
clip-path: polygon(0 58%, 100% 58%, 100% 82%, 0 82%);
}
86% {
opacity: 0.6;
transform: translateX(5px);
}
88% {
opacity: 0;
}
}
.gt-13__sub {
font-size: 12px;
letter-spacing: 5px;
color: rgba(249,115,22,0.5);
text-transform: uppercase;
mix-blend-mode: screen;
}
@media (prefers-reduced-motion: reduce) {
.gt-13__word,
.gt-13__word::before,
.gt-13__word::after {
animation: none;
}
.gt-13__word::before,
.gt-13__word::after {
opacity: 0;
}
.gt-13__noise {
animation: none;
}
}(function() {
const canvas = document.getElementById('gt-13-noise');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let animId;
function resize() {
const parent = canvas.parentElement;
canvas.width = parent.offsetWidth;
canvas.height = parent.offsetHeight;
}
function drawNoise() {
const w = canvas.width;
const h = canvas.height;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = Math.random() * 255 | 0;
data[i] = v;
data[i+1] = (v * 0.6) | 0; // warmer grain
data[i+2] = (v * 0.3) | 0;
data[i+3] = 255;
}
ctx.putImageData(imageData, 0, 0);
animId = requestAnimationFrame(drawNoise);
}
resize();
drawNoise();
window.addEventListener('resize', resize);
/* Pause when off-screen for perf */
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) { cancelAnimationFrame(animId); animId = null; }
else if (!animId) drawNoise();
});
});
obs.observe(canvas);
})();(function() {
const canvas = document.getElementById('gt-13-noise');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let animId;
function resize() {
const parent = canvas.parentElement;
canvas.width = parent.offsetWidth;
canvas.height = parent.offsetHeight;
}
function drawNoise() {
const w = canvas.width;
const h = canvas.height;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = Math.random() * 255 | 0;
data[i] = v;
data[i+1] = (v * 0.6) | 0; // warmer grain
data[i+2] = (v * 0.3) | 0;
data[i+3] = 255;
}
ctx.putImageData(imageData, 0, 0);
animId = requestAnimationFrame(drawNoise);
}
resize();
drawNoise();
window.addEventListener('resize', resize);
/* Pause when off-screen for perf */
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) { cancelAnimationFrame(animId); animId = null; }
else if (!animId) drawNoise();
});
});
obs.observe(canvas);
})();Here's a working CSS Glitch Text Effect 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: Glitch Text with Noise Overlay
Source: https://codefronts.com/motion/css-glitch-text-effect/glitch-text-with-noise-overlay/
Animated pixel noise generated every frame by a canvas element fills the background, while the overlaid headline uses mix-blend-mode:screen to cut through the grain with a warm orange channel-split glitch that erupts periodically.
## HTML
```html
<div class="gt-13">
<canvas class="gt-13__noise" id="gt-13-noise"></canvas>
<div class="gt-13__stage">
<div class="gt-13__word" data-text="FRACTAL">FRACTAL</div>
<div class="gt-13__sub">noise · signal · entropy</div>
</div>
</div>
```
## CSS
```css
.gt-13,
.gt-13 *,
.gt-13 *::before,
.gt-13 *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.gt-13 ::selection {
background: #f97316;
color: #000;
}
.gt-13 {
--orange: #f97316;
--yellow: #fbbf24;
--bg: #09060a;
--text: #fff9f0;
min-height: 100vh;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 48px 20px;
font-family: 'Inter', system-ui, sans-serif;
position: relative;
overflow: hidden;
}
/* Noise canvas fills the bg. opacity bumped 0.18 -> 0.35 so the
warm-grain texture is clearly visible behind the title even in
static thumbnails; the live demo still feels atmospheric, the
gallery card now actually advertises the noise overlay effect. */
.gt-13__noise {
position: absolute;
inset: 0;
opacity: 0.35;
pointer-events: none;
z-index: 0;
}
.gt-13__stage {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
/* Main word cuts through noise. Font scaled up clamp min 64px (was
48px) and max 120px (was 96px) so the title fills more of the
capture frame. Permanent dual-shadow (orange offset left, yellow
offset right) gives a baseline chromatic split so the gallery
thumbnail reads as a glitch even when caught between the slice
keyframes; the animated slice pseudos still layer on top during
live playback. */
.gt-13__word {
position: relative;
font-size: clamp(64px, 12vw, 120px);
font-weight: 900;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--text);
/* Text cuts through noise via mix-blend-mode */
mix-blend-mode: screen;
text-shadow: -3px 0 0 rgba(249,115,22,0.85), 3px 0 0 rgba(251,191,36,0.85), 0 0 20px rgba(249,115,22,0.4);
animation: gt-13-main-glitch 4s steps(1) infinite;
}
.gt-13__word::before {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--orange);
mix-blend-mode: screen;
clip-path: polygon(0 15%, 100% 15%, 100% 38%, 0 38%);
animation: gt-13-slice-a 4s steps(1) infinite;
}
.gt-13__word::after {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--yellow);
mix-blend-mode: screen;
clip-path: polygon(0 62%, 100% 62%, 100% 85%, 0 85%);
animation: gt-13-slice-b 4s steps(1) infinite;
}
@keyframes gt-13-main-glitch {
0%,
80%,
100% {
transform: translateX(0);
}
82% {
transform: translateX(-6px) skewX(-2deg);
}
84% {
transform: translateX(5px);
}
86% {
transform: translateX(-2px) skewX(1deg);
}
88% {
transform: translateX(0);
}
}
@keyframes gt-13-slice-a {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(-14px);
clip-path: polygon(0 10%, 100% 10%, 100% 35%, 0 35%);
}
84% {
opacity: 1;
transform: translateX(10px);
clip-path: polygon(0 18%, 100% 18%, 100% 40%, 0 40%);
}
86% {
opacity: 0.6;
transform: translateX(-4px);
}
88% {
opacity: 0;
}
}
@keyframes gt-13-slice-b {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(16px);
clip-path: polygon(0 65%, 100% 65%, 100% 88%, 0 88%);
}
84% {
opacity: 1;
transform: translateX(-10px);
clip-path: polygon(0 58%, 100% 58%, 100% 82%, 0 82%);
}
86% {
opacity: 0.6;
transform: translateX(5px);
}
88% {
opacity: 0;
}
}
.gt-13__sub {
font-size: 12px;
letter-spacing: 5px;
color: rgba(249,115,22,0.5);
text-transform: uppercase;
mix-blend-mode: screen;
}
@media (prefers-reduced-motion: reduce) {
.gt-13__word,
.gt-13__word::before,
.gt-13__word::after {
animation: none;
}
.gt-13__word::before,
.gt-13__word::after {
opacity: 0;
}
.gt-13__noise {
animation: none;
}
}
```
## JavaScript
```js
(function() {
const canvas = document.getElementById('gt-13-noise');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let animId;
function resize() {
const parent = canvas.parentElement;
canvas.width = parent.offsetWidth;
canvas.height = parent.offsetHeight;
}
function drawNoise() {
const w = canvas.width;
const h = canvas.height;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = Math.random() * 255 | 0;
data[i] = v;
data[i+1] = (v * 0.6) | 0; // warmer grain
data[i+2] = (v * 0.3) | 0;
data[i+3] = 255;
}
ctx.putImageData(imageData, 0, 0);
animId = requestAnimationFrame(drawNoise);
}
resize();
drawNoise();
window.addEventListener('resize', resize);
/* Pause when off-screen for perf */
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) { cancelAnimationFrame(animId); animId = null; }
else if (!animId) drawNoise();
});
});
obs.observe(canvas);
})();
```Here's a working CSS Glitch Text Effect 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: Glitch Text with Noise Overlay
Source: https://codefronts.com/motion/css-glitch-text-effect/glitch-text-with-noise-overlay/
Animated pixel noise generated every frame by a canvas element fills the background, while the overlaid headline uses mix-blend-mode:screen to cut through the grain with a warm orange channel-split glitch that erupts periodically.
## HTML
```html
<div class="gt-13">
<canvas class="gt-13__noise" id="gt-13-noise"></canvas>
<div class="gt-13__stage">
<div class="gt-13__word" data-text="FRACTAL">FRACTAL</div>
<div class="gt-13__sub">noise · signal · entropy</div>
</div>
</div>
```
## CSS
```css
.gt-13,
.gt-13 *,
.gt-13 *::before,
.gt-13 *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.gt-13 ::selection {
background: #f97316;
color: #000;
}
.gt-13 {
--orange: #f97316;
--yellow: #fbbf24;
--bg: #09060a;
--text: #fff9f0;
min-height: 100vh;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
padding: 48px 20px;
font-family: 'Inter', system-ui, sans-serif;
position: relative;
overflow: hidden;
}
/* Noise canvas fills the bg. opacity bumped 0.18 -> 0.35 so the
warm-grain texture is clearly visible behind the title even in
static thumbnails; the live demo still feels atmospheric, the
gallery card now actually advertises the noise overlay effect. */
.gt-13__noise {
position: absolute;
inset: 0;
opacity: 0.35;
pointer-events: none;
z-index: 0;
}
.gt-13__stage {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
/* Main word cuts through noise. Font scaled up clamp min 64px (was
48px) and max 120px (was 96px) so the title fills more of the
capture frame. Permanent dual-shadow (orange offset left, yellow
offset right) gives a baseline chromatic split so the gallery
thumbnail reads as a glitch even when caught between the slice
keyframes; the animated slice pseudos still layer on top during
live playback. */
.gt-13__word {
position: relative;
font-size: clamp(64px, 12vw, 120px);
font-weight: 900;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--text);
/* Text cuts through noise via mix-blend-mode */
mix-blend-mode: screen;
text-shadow: -3px 0 0 rgba(249,115,22,0.85), 3px 0 0 rgba(251,191,36,0.85), 0 0 20px rgba(249,115,22,0.4);
animation: gt-13-main-glitch 4s steps(1) infinite;
}
.gt-13__word::before {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--orange);
mix-blend-mode: screen;
clip-path: polygon(0 15%, 100% 15%, 100% 38%, 0 38%);
animation: gt-13-slice-a 4s steps(1) infinite;
}
.gt-13__word::after {
content: attr(data-text);
position: absolute;
inset: 0;
font: inherit;
letter-spacing: inherit;
color: var(--yellow);
mix-blend-mode: screen;
clip-path: polygon(0 62%, 100% 62%, 100% 85%, 0 85%);
animation: gt-13-slice-b 4s steps(1) infinite;
}
@keyframes gt-13-main-glitch {
0%,
80%,
100% {
transform: translateX(0);
}
82% {
transform: translateX(-6px) skewX(-2deg);
}
84% {
transform: translateX(5px);
}
86% {
transform: translateX(-2px) skewX(1deg);
}
88% {
transform: translateX(0);
}
}
@keyframes gt-13-slice-a {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(-14px);
clip-path: polygon(0 10%, 100% 10%, 100% 35%, 0 35%);
}
84% {
opacity: 1;
transform: translateX(10px);
clip-path: polygon(0 18%, 100% 18%, 100% 40%, 0 40%);
}
86% {
opacity: 0.6;
transform: translateX(-4px);
}
88% {
opacity: 0;
}
}
@keyframes gt-13-slice-b {
0%,
80%,
100% {
opacity: 0;
transform: translateX(0);
}
82% {
opacity: 1;
transform: translateX(16px);
clip-path: polygon(0 65%, 100% 65%, 100% 88%, 0 88%);
}
84% {
opacity: 1;
transform: translateX(-10px);
clip-path: polygon(0 58%, 100% 58%, 100% 82%, 0 82%);
}
86% {
opacity: 0.6;
transform: translateX(5px);
}
88% {
opacity: 0;
}
}
.gt-13__sub {
font-size: 12px;
letter-spacing: 5px;
color: rgba(249,115,22,0.5);
text-transform: uppercase;
mix-blend-mode: screen;
}
@media (prefers-reduced-motion: reduce) {
.gt-13__word,
.gt-13__word::before,
.gt-13__word::after {
animation: none;
}
.gt-13__word::before,
.gt-13__word::after {
opacity: 0;
}
.gt-13__noise {
animation: none;
}
}
```
## JavaScript
```js
(function() {
const canvas = document.getElementById('gt-13-noise');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let animId;
function resize() {
const parent = canvas.parentElement;
canvas.width = parent.offsetWidth;
canvas.height = parent.offsetHeight;
}
function drawNoise() {
const w = canvas.width;
const h = canvas.height;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = Math.random() * 255 | 0;
data[i] = v;
data[i+1] = (v * 0.6) | 0; // warmer grain
data[i+2] = (v * 0.3) | 0;
data[i+3] = 255;
}
ctx.putImageData(imageData, 0, 0);
animId = requestAnimationFrame(drawNoise);
}
resize();
drawNoise();
window.addEventListener('resize', resize);
/* Pause when off-screen for perf */
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) { cancelAnimationFrame(animId); animId = null; }
else if (!animId) drawNoise();
});
});
obs.observe(canvas);
})();
```How this works
A full-size canvas element is positioned absolutely behind the content layer. On every animation frame, ctx.createImageData() generates a new frame of RGB noise by writing random byte values to the pixel data array -- the red channel receives full amplitude while green gets 60% and blue 30%, warming the grain toward orange-brown. The canvas animates at the display native refresh rate via requestAnimationFrame, producing true per-frame film grain rather than a looping GIF.
The headline and its pseudo-element channels all carry mix-blend-mode: screen, which adds their colour values to the canvas pixels beneath rather than replacing them -- dark canvas pixels become fully transparent to the text, while bright noise pixels lighten the text in those areas, creating the impression of letters physically embedded in the noise matrix. An IntersectionObserver pauses the loop when the canvas exits the viewport to prevent wasted GPU time.
Make it yours
- Change the grain colour temperature by adjusting the channel multipliers: set all three to
vfor neutral grey grain, or boost blue tov * 0.8for a colder silver-screen look. - Reduce canvas resolution for better performance by setting
canvas.width = parent.offsetWidth / 2and usingctx.scale(2, 2)to upscale -- this halves pixel fill cost at the cost of larger grain texture. - Replace
mix-blend-mode: screenwithmix-blend-mode: overlayfor a grittier look where the noise visually textures the letterforms. - Add
filter: contrast(1.5)to the wrapper to push the noise toward black and white, creating a high-contrast film-grain effect. - Cap the noise opacity at
0.08instead of0.18for an ultra-subtle background texture suitable for production landing pages.
Gotchas — read before shipping
- Writing to
ImageDataon everyrequestAnimationFrameis inherently CPU-bound -- on mobile devices this can consume 15-25% of a CPU core continuously. Always use theIntersectionObserverpause pattern. mix-blend-mode: screenrequires both the canvas and text to be in the same stacking context; any ancestor withisolation: isolateorwill-change: transformwill break the blend.- Add
aria-hidden="true"to the canvas element to prevent screen readers from announcing it, and ensure the text overlay has sufficient colour contrast against the worst-case noise brightness.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 80+ | 14+ | 78+ | 80+ |
mix-blend-mode: screen and IntersectionObserver are both widely supported; canvas 2D is universal.