UI Components Medium
Video Text
Text that acts as a mask showing video or animated gradients through the letter shapes using mix-blend-mode and background-clip techniques.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-text-container {
position: relative;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* Video layer — sits behind the text */
.video-text-container video,
.video-text-container .video-text-gradient {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
/* Animated gradient fallback */
.video-text-gradient {
background: linear-gradient(
135deg,
#a78bfa 0%,
#ec4899 25%,
#f59e0b 50%,
#10b981 75%,
#3b82f6 100%
);
background-size: 400% 400%;
animation: gradient-shift 8s ease infinite;
}
@keyframes gradient-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/*
Text overlay — dark background with mix-blend-mode: screen.
Dark pixels become transparent, white text reveals the video.
*/
.video-text-overlay {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: #0a0a0a;
mix-blend-mode: screen;
}
.video-text-overlay h1 {
font-size: clamp(4rem, 15vw, 12rem);
font-weight: 900;
letter-spacing: -0.04em;
color: #fff;
text-align: center;
text-transform: uppercase;
line-height: 0.9;
padding: 0 1rem;
}
/* Subtle scanlines for extra style */
.video-text-container::after {
content: "";
position: absolute;
inset: 0;
z-index: 3;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.03) 2px,
rgba(0, 0, 0, 0.03) 4px
);
}/**
* Video Text
* Sets up the video element behind the text overlay.
* Falls back to an animated gradient if the video cannot load.
*/
(function () {
const container = document.querySelector(".video-text-container");
if (!container) return;
const videoSrc = container.dataset.videoSrc || "";
const mediaSlot = container.querySelector(".video-text-media");
if (!mediaSlot) return;
if (videoSrc) {
const video = document.createElement("video");
video.src = videoSrc;
video.autoplay = true;
video.loop = true;
video.muted = true;
video.playsInline = true;
video.setAttribute("playsinline", "");
video.addEventListener("error", () => {
// Replace with gradient fallback on error
video.remove();
addGradientFallback(mediaSlot);
});
mediaSlot.appendChild(video);
} else {
addGradientFallback(mediaSlot);
}
function addGradientFallback(slot) {
const div = document.createElement("div");
div.classList.add("video-text-gradient");
slot.appendChild(div);
}
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Video Text</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!--
Set data-video-src to a video URL to show video through text.
Leave empty to use the animated gradient fallback.
Example: data-video-src="https://example.com/video.mp4"
-->
<div class="video-text-container" data-video-src="">
<!-- Media layer (video or gradient inserted by JS) -->
<div class="video-text-media"></div>
<!-- Text overlay with blend mode -->
<div class="video-text-overlay">
<h1>STEAL<br />THIS</h1>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useRef, useEffect } from "react";
interface VideoTextProps {
text?: string;
videoSrc?: string;
}
export default function VideoText({ text = "STEAL\nTHIS", videoSrc }: VideoTextProps) {
const [useFallback, setUseFallback] = useState(!videoSrc);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (videoRef.current) {
videoRef.current.play().catch(() => setUseFallback(true));
}
}, [videoSrc]);
const lines = text.split("\n");
return (
<div
style={{
position: "relative",
width: "100%",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
background: "#0a0a0a",
}}
>
{/* Media layer */}
{useFallback ? (
<div
style={{
position: "absolute",
inset: 0,
zIndex: 1,
background:
"linear-gradient(135deg, #a78bfa 0%, #ec4899 25%, #f59e0b 50%, #10b981 75%, #3b82f6 100%)",
backgroundSize: "400% 400%",
animation: "videoTextGradient 8s ease infinite",
}}
/>
) : (
<video
ref={videoRef}
src={videoSrc}
autoPlay
loop
muted
playsInline
onError={() => setUseFallback(true)}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
zIndex: 1,
}}
/>
)}
{/* Text overlay */}
<div
style={{
position: "relative",
zIndex: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
background: "#0a0a0a",
mixBlendMode: "screen",
}}
>
<h1
style={{
fontSize: "clamp(4rem, 15vw, 12rem)",
fontWeight: 900,
letterSpacing: "-0.04em",
color: "#fff",
textAlign: "center",
textTransform: "uppercase",
lineHeight: 0.9,
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
{lines.map((line, i) => (
<span key={i}>
{line}
{i < lines.length - 1 && <br />}
</span>
))}
</h1>
</div>
{/* Subtle scanlines */}
<div
style={{
position: "absolute",
inset: 0,
zIndex: 3,
pointerEvents: "none",
background:
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px)",
}}
/>
<style>{`
@keyframes videoTextGradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
`}</style>
</div>
);
}<script setup>
import { ref, computed, onMounted } from "vue";
const props = defineProps({
text: { type: String, default: "STEAL\nTHIS" },
videoSrc: { type: String, default: undefined },
});
const useFallback = ref(!props.videoSrc);
const videoEl = ref(null);
const lines = computed(() => props.text.split("\n"));
onMounted(() => {
if (videoEl.value && props.videoSrc) {
videoEl.value.play().catch(() => {
useFallback.value = true;
});
}
});
function onVideoError() {
useFallback.value = true;
}
</script>
<template>
<div
style="position: relative; width: 100%; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #0a0a0a;"
>
<div
v-if="useFallback"
class="gradient-bg"
style="position: absolute; inset: 0; z-index: 1; background: linear-gradient(135deg, #a78bfa 0%, #ec4899 25%, #f59e0b 50%, #10b981 75%, #3b82f6 100%); background-size: 400% 400%;"
/>
<video
v-else
ref="videoEl"
:src="props.videoSrc"
autoplay
loop
muted
playsinline
@error="onVideoError"
style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1;"
/>
<div
style="position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: #0a0a0a; mix-blend-mode: screen;"
>
<h1
style="font-size: clamp(4rem, 15vw, 12rem); font-weight: 900; letter-spacing: -0.04em; color: #fff; text-align: center; text-transform: uppercase; line-height: 0.9; font-family: system-ui, -apple-system, sans-serif;"
>
<span v-for="(line, i) in lines" :key="i">
{{ line }}<br v-if="i < lines.length - 1" />
</span>
</h1>
</div>
<div
style="position: absolute; inset: 0; z-index: 3; pointer-events: none; background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);"
/>
</div>
</template>
<style scoped>
.gradient-bg {
animation: videoTextGradient 8s ease infinite;
}
@keyframes videoTextGradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
</style><script>
import { onMount } from "svelte";
export let text = "STEAL\nTHIS";
export let videoSrc = undefined;
let useFallback = !videoSrc;
let videoEl;
$: lines = text.split("\n");
onMount(() => {
if (videoEl && videoSrc) {
videoEl.play().catch(() => {
useFallback = true;
});
}
});
function onVideoError() {
useFallback = true;
}
</script>
<div
style="position: relative; width: 100%; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; background: #0a0a0a;"
>
{#if useFallback}
<div
class="gradient-bg"
style="position: absolute; inset: 0; z-index: 1; background: linear-gradient(135deg, #a78bfa 0%, #ec4899 25%, #f59e0b 50%, #10b981 75%, #3b82f6 100%); background-size: 400% 400%;"
/>
{:else}
<!-- svelte-ignore a11y-media-has-caption -->
<video
bind:this={videoEl}
src={videoSrc}
autoplay
loop
muted
playsinline
on:error={onVideoError}
style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1;"
/>
{/if}
<div
style="position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: #0a0a0a; mix-blend-mode: screen;"
>
<h1
style="font-size: clamp(4rem, 15vw, 12rem); font-weight: 900; letter-spacing: -0.04em; color: #fff; text-align: center; text-transform: uppercase; line-height: 0.9; font-family: system-ui, -apple-system, sans-serif;"
>
{#each lines as line, i}
<span>{line}{#if i < lines.length - 1}<br />{/if}</span>
{/each}
</h1>
</div>
<div
style="position: absolute; inset: 0; z-index: 3; pointer-events: none; background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);"
/>
</div>
<style>
.gradient-bg {
animation: videoTextGradient 8s ease infinite;
}
@keyframes videoTextGradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
</style>Video Text
Large text that reveals a video (or animated gradient fallback) through the letter shapes. Uses mix-blend-mode: screen to knock out the background and show the moving imagery only inside the characters.
How it works
A <video> element plays behind a text overlay. The text layer uses a solid dark background with mix-blend-mode: screen, making the dark area transparent and revealing the video only through the white letterforms.
Features
- Video reveal — any video URL plays through text shapes
- Gradient fallback — animated gradient when no video is available
- mix-blend-mode — no canvas or SVG masks required
- Responsive — scales with viewport width