08 / 20
All 20 in 20 Tailwind Retro UI
- 01Tailwind CRT Scanlines and Screen Curvature
- 02Tailwind Pixel Art Scaling with Image Rendering
- 03Tailwind Dithering Patterns with Gradients
- 04Tailwind One Bit Bevel Borders with Box Shadow
- 05Tailwind Classic Mac System 7 Window Chrome
- 06Tailwind MS DOS Text Mode Interface Grid
- 07Tailwind Teletext and Ceefax Block Graphics
- 08Tailwind Amber and Green Phosphor Monitor
- 09Tailwind Windows 95 Desktop Interface
- 10Tailwind Retro Futuristic Terminal Interface
- 11Tailwind Eight Bit Pixel Art Buttons and HUD
- 12Tailwind Neo Brutalist Component Board
- 13Tailwind Skeuomorphic Cassette Player
- 14Tailwind Vaporwave Aesthetic Scene
- 15Tailwind Retro Arcade Game Interface
- 16Tailwind ASCII Art Terminal Interface
- 17Tailwind Nineties Web Guestbook Widget
- 18Tailwind Y2K Aqua Gloss Interface Kit
- 19Tailwind Vintage Storefront Layout
- 20Tailwind Retro Desktop Portfolio Template
Tailwind Amber and Green Phosphor Monitor
Two monochrome tubes side by side with the phosphor named on each: P1 green and P3 amber. Persistence is a real trailing fade on newly written lines rather than a static glow, and the port changes how the script talks to the CSS — it sets a data- attribute and a data-* variant does the styling, which is the pattern to reach for whenever JS and utilities meet.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="twr-08 relative block w-full min-h-svh box-border bg-twr08-bg text-twr08-ink font-twr08-mono" aria-labelledby="twr-08-title">
<h2 class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap" id="twr-08-title">Monochrome phosphor monitors, P1 green and P3 amber</h2>
<div class="absolute top-4 right-4 z-6">
<input class="peer absolute inset-0 size-11 m-0 opacity-0 cursor-pointer" type="checkbox" id="twr-08-theme">
<label class="grid place-items-center size-11 rounded-full border border-twr08-line bg-twr08-surface text-twr08-ink2 cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-twr08-acc peer-focus-visible:outline-offset-2" for="twr-08-theme">
<svg class="size-5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"/></svg>
<span class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap">Toggle light surround</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] gap-6 max-w-[74rem] mx-auto px-[clamp(0.85rem,0.5rem+2vw,2rem)] pt-[clamp(3.5rem,3rem+3vw,5rem)] pb-16">
<header class="grid gap-1.5 min-w-0">
<p class="m-0 text-[0.68rem] tracking-[0.14em] uppercase text-twr08-ink2">Monochrome CRT · phosphor persistence and bloom</p>
<h3 class="m-0 min-w-0 text-balance text-[clamp(1.3rem,1rem+1.9vw,2.05rem)] font-semibold leading-tight tracking-[-0.02em]">Two phosphors, one code path</h3>
</header>
<div class="grid grid-cols-[minmax(0,1fr)] gap-4.5 md:grid-cols-2">
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#3c3f3a] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#33ff66] [--twr-08-p-dim:#1c7a3a] [--twr-08-decay:1400ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l1">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p1" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l1">P1 · zinc silicate green</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">long — 1.4 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">525 nm</dd></div>
</dl>
</div>
</article>
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#423d34] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#ffb000] [--twr-08-p-dim:#8a5f00] [--twr-08-decay:600ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l2">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p3" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l2">P3 · zinc beryllium amber</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">medium — 0.6 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">602 nm</dd></div>
</dl>
</div>
</article>
</div>
<template data-twr-08-line>
<p class="m-0 min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-[var(--twr-08-p-dim)] transition-[color,text-shadow] duration-[var(--twr-08-decay)] ease-out motion-reduce:transition-none data-fresh:text-[var(--twr-08-p)] data-fresh:[text-shadow:0_0_3px_var(--twr-08-p),0_0_9px_var(--twr-08-p)]"><span class="inline-block w-[3ch]" data-mark></span><span data-text></span></p>
</template>
<aside class="min-w-0 grid gap-2.5 border border-twr08-line bg-twr08-surface p-[clamp(0.85rem,0.6rem+1vw,1.2rem)]">
<h4 class="m-0 text-[0.72rem] tracking-[0.1em] uppercase text-twr08-ink2">State without colour</h4>
<p class="m-0 min-w-0 max-w-[68ch] text-[0.85rem] leading-relaxed text-pretty text-twr08-ink2">One hue means no colour coding. Every line below marks its state with a bracket glyph in a fixed column and a written word, so the distinction survives on a display that has exactly one colour to give.</p>
<ul class="list-none m-0 p-0 flex flex-wrap gap-x-5 gap-y-1.5 text-[0.8rem]">
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[ ]</span> routine</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[!]</span> warning</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[x]</span> fault</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[>]</span> awaiting input</li>
</ul>
</aside>
</div>
</section><section class="twr-08 relative block w-full min-h-svh box-border bg-twr08-bg text-twr08-ink font-twr08-mono" aria-labelledby="twr-08-title">
<h2 class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap" id="twr-08-title">Monochrome phosphor monitors, P1 green and P3 amber</h2>
<div class="absolute top-4 right-4 z-6">
<input class="peer absolute inset-0 size-11 m-0 opacity-0 cursor-pointer" type="checkbox" id="twr-08-theme">
<label class="grid place-items-center size-11 rounded-full border border-twr08-line bg-twr08-surface text-twr08-ink2 cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-twr08-acc peer-focus-visible:outline-offset-2" for="twr-08-theme">
<svg class="size-5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"/></svg>
<span class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap">Toggle light surround</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] gap-6 max-w-[74rem] mx-auto px-[clamp(0.85rem,0.5rem+2vw,2rem)] pt-[clamp(3.5rem,3rem+3vw,5rem)] pb-16">
<header class="grid gap-1.5 min-w-0">
<p class="m-0 text-[0.68rem] tracking-[0.14em] uppercase text-twr08-ink2">Monochrome CRT · phosphor persistence and bloom</p>
<h3 class="m-0 min-w-0 text-balance text-[clamp(1.3rem,1rem+1.9vw,2.05rem)] font-semibold leading-tight tracking-[-0.02em]">Two phosphors, one code path</h3>
</header>
<div class="grid grid-cols-[minmax(0,1fr)] gap-4.5 md:grid-cols-2">
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#3c3f3a] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#33ff66] [--twr-08-p-dim:#1c7a3a] [--twr-08-decay:1400ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l1">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p1" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l1">P1 · zinc silicate green</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">long — 1.4 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">525 nm</dd></div>
</dl>
</div>
</article>
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#423d34] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#ffb000] [--twr-08-p-dim:#8a5f00] [--twr-08-decay:600ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l2">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p3" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l2">P3 · zinc beryllium amber</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">medium — 0.6 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">602 nm</dd></div>
</dl>
</div>
</article>
</div>
<template data-twr-08-line>
<p class="m-0 min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-[var(--twr-08-p-dim)] transition-[color,text-shadow] duration-[var(--twr-08-decay)] ease-out motion-reduce:transition-none data-fresh:text-[var(--twr-08-p)] data-fresh:[text-shadow:0_0_3px_var(--twr-08-p),0_0_9px_var(--twr-08-p)]"><span class="inline-block w-[3ch]" data-mark></span><span data-text></span></p>
</template>
<aside class="min-w-0 grid gap-2.5 border border-twr08-line bg-twr08-surface p-[clamp(0.85rem,0.6rem+1vw,1.2rem)]">
<h4 class="m-0 text-[0.72rem] tracking-[0.1em] uppercase text-twr08-ink2">State without colour</h4>
<p class="m-0 min-w-0 max-w-[68ch] text-[0.85rem] leading-relaxed text-pretty text-twr08-ink2">One hue means no colour coding. Every line below marks its state with a bracket glyph in a fixed column and a written word, so the distinction survives on a display that has exactly one colour to give.</p>
<ul class="list-none m-0 p-0 flex flex-wrap gap-x-5 gap-y-1.5 text-[0.8rem]">
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[ ]</span> routine</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[!]</span> warning</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[x]</span> fault</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[>]</span> awaiting input</li>
</ul>
</aside>
</div>
</section>@import "tailwindcss";
@theme {
--color-twr08-bg: #1a1c1e;
--color-twr08-bg: oklch(0.23 0.004 220);
--color-twr08-surface: #262a2d;
--color-twr08-surface: oklch(0.30 0.006 220);
--color-twr08-ink: #eef1f2;
--color-twr08-ink: oklch(0.96 0.003 220);
--color-twr08-ink2: #aab2b6;
--color-twr08-ink2: oklch(0.76 0.008 220);
--color-twr08-line: #3e4548;
--color-twr08-line: oklch(0.37 0.008 220);
--color-twr08-acc: #86d7c4;
--color-twr08-acc: oklch(0.84 0.09 172);
--font-twr08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--animate-twr08-retrace: twr-08-retrace 7s linear infinite;
}
@keyframes twr-08-retrace {
from {
transform: translateY(-120%);
}
to {
transform: translateY(560%);
}
}
.twr-08:has(#twr-08-theme:checked) {
--color-twr08-bg: #dfe2e3;
--color-twr08-bg: oklch(0.90 0.004 220);
--color-twr08-surface: #f4f6f6;
--color-twr08-surface: oklch(0.97 0.002 220);
--color-twr08-ink: #16191b;
--color-twr08-ink: oklch(0.20 0.005 220);
--color-twr08-ink2: #4a5255;
--color-twr08-ink2: oklch(0.43 0.009 220);
--color-twr08-line: #aab1b4;
--color-twr08-line: oklch(0.75 0.007 220);
--color-twr08-acc: #0d6d5b;
--color-twr08-acc: oklch(0.48 0.1 172);
}@import "tailwindcss";
@theme {
--color-twr08-bg: #1a1c1e;
--color-twr08-bg: oklch(0.23 0.004 220);
--color-twr08-surface: #262a2d;
--color-twr08-surface: oklch(0.30 0.006 220);
--color-twr08-ink: #eef1f2;
--color-twr08-ink: oklch(0.96 0.003 220);
--color-twr08-ink2: #aab2b6;
--color-twr08-ink2: oklch(0.76 0.008 220);
--color-twr08-line: #3e4548;
--color-twr08-line: oklch(0.37 0.008 220);
--color-twr08-acc: #86d7c4;
--color-twr08-acc: oklch(0.84 0.09 172);
--font-twr08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--animate-twr08-retrace: twr-08-retrace 7s linear infinite;
}
@keyframes twr-08-retrace {
from {
transform: translateY(-120%);
}
to {
transform: translateY(560%);
}
}
.twr-08:has(#twr-08-theme:checked) {
--color-twr08-bg: #dfe2e3;
--color-twr08-bg: oklch(0.90 0.004 220);
--color-twr08-surface: #f4f6f6;
--color-twr08-surface: oklch(0.97 0.002 220);
--color-twr08-ink: #16191b;
--color-twr08-ink: oklch(0.20 0.005 220);
--color-twr08-ink2: #4a5255;
--color-twr08-ink2: oklch(0.43 0.009 220);
--color-twr08-line: #aab1b4;
--color-twr08-line: oklch(0.75 0.007 220);
--color-twr08-acc: #0d6d5b;
--color-twr08-acc: oklch(0.48 0.1 172);
}/* The script sets state only — a data attribute — and never a style or a class.
Appearance lives in data-fresh: variants on the template line. */
(function () {
var root = document.querySelector('.twr-08');
if (!root) return;
var tpl = root.querySelector('[data-twr-08-line]');
var feeds = Array.prototype.slice.call(root.querySelectorAll('[data-twr-08-feed]'));
var lines = [
['[ ]', 'MEM CHECK 640K OK'], ['[ ]', 'SEEK TRK 00 SEC 01'],
['[!]', 'PARITY RETRY 1 OF 3'], ['[ ]', 'BUFFER FLUSHED 4096B'],
['[x]', 'DEVICE 2 NOT READY'], ['[ ]', 'LINK 1200 BAUD 8N1'],
['[>]', 'AWAITING OPERATOR'], ['[ ]', 'SPOOL QUEUE EMPTY']
];
var i = 0;
function write() {
var pair = lines[i % lines.length];
feeds.forEach(function (feed) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.querySelector('[data-mark]').textContent = pair[0];
node.querySelector('[data-text]').textContent = pair[1];
node.setAttribute('data-fresh', '');
feed.appendChild(node);
requestAnimationFrame(function () {
requestAnimationFrame(function () { node.removeAttribute('data-fresh'); });
});
while (feed.childElementCount > 9) feed.removeChild(feed.firstElementChild);
});
i += 1;
}
write();
var timer = setInterval(write, 1500);
root.__twrCleanup = function () {
clearInterval(timer);
feeds.forEach(function (f) { f.textContent = ''; });
};
})();/* The script sets state only — a data attribute — and never a style or a class.
Appearance lives in data-fresh: variants on the template line. */
(function () {
var root = document.querySelector('.twr-08');
if (!root) return;
var tpl = root.querySelector('[data-twr-08-line]');
var feeds = Array.prototype.slice.call(root.querySelectorAll('[data-twr-08-feed]'));
var lines = [
['[ ]', 'MEM CHECK 640K OK'], ['[ ]', 'SEEK TRK 00 SEC 01'],
['[!]', 'PARITY RETRY 1 OF 3'], ['[ ]', 'BUFFER FLUSHED 4096B'],
['[x]', 'DEVICE 2 NOT READY'], ['[ ]', 'LINK 1200 BAUD 8N1'],
['[>]', 'AWAITING OPERATOR'], ['[ ]', 'SPOOL QUEUE EMPTY']
];
var i = 0;
function write() {
var pair = lines[i % lines.length];
feeds.forEach(function (feed) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.querySelector('[data-mark]').textContent = pair[0];
node.querySelector('[data-text]').textContent = pair[1];
node.setAttribute('data-fresh', '');
feed.appendChild(node);
requestAnimationFrame(function () {
requestAnimationFrame(function () { node.removeAttribute('data-fresh'); });
});
while (feed.childElementCount > 9) feed.removeChild(feed.firstElementChild);
});
i += 1;
}
write();
var timer = setInterval(write, 1500);
root.__twrCleanup = function () {
clearInterval(timer);
feeds.forEach(function (f) { f.textContent = ''; });
};
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-twr08-bg: #1a1c1e;
--color-twr08-bg: oklch(0.23 0.004 220);
--color-twr08-surface: #262a2d;
--color-twr08-surface: oklch(0.30 0.006 220);
--color-twr08-ink: #eef1f2;
--color-twr08-ink: oklch(0.96 0.003 220);
--color-twr08-ink2: #aab2b6;
--color-twr08-ink2: oklch(0.76 0.008 220);
--color-twr08-line: #3e4548;
--color-twr08-line: oklch(0.37 0.008 220);
--color-twr08-acc: #86d7c4;
--color-twr08-acc: oklch(0.84 0.09 172);
--font-twr08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--animate-twr08-retrace: twr-08-retrace 7s linear infinite;
}
@keyframes twr-08-retrace {
from {
transform: translateY(-120%);
}
to {
transform: translateY(560%);
}
}
.twr-08:has(#twr-08-theme:checked) {
--color-twr08-bg: #dfe2e3;
--color-twr08-bg: oklch(0.90 0.004 220);
--color-twr08-surface: #f4f6f6;
--color-twr08-surface: oklch(0.97 0.002 220);
--color-twr08-ink: #16191b;
--color-twr08-ink: oklch(0.20 0.005 220);
--color-twr08-ink2: #4a5255;
--color-twr08-ink2: oklch(0.43 0.009 220);
--color-twr08-line: #aab1b4;
--color-twr08-line: oklch(0.75 0.007 220);
--color-twr08-acc: #0d6d5b;
--color-twr08-acc: oklch(0.48 0.1 172);
}@import "tailwindcss";
@theme {
--color-twr08-bg: #1a1c1e;
--color-twr08-bg: oklch(0.23 0.004 220);
--color-twr08-surface: #262a2d;
--color-twr08-surface: oklch(0.30 0.006 220);
--color-twr08-ink: #eef1f2;
--color-twr08-ink: oklch(0.96 0.003 220);
--color-twr08-ink2: #aab2b6;
--color-twr08-ink2: oklch(0.76 0.008 220);
--color-twr08-line: #3e4548;
--color-twr08-line: oklch(0.37 0.008 220);
--color-twr08-acc: #86d7c4;
--color-twr08-acc: oklch(0.84 0.09 172);
--font-twr08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--animate-twr08-retrace: twr-08-retrace 7s linear infinite;
}
@keyframes twr-08-retrace {
from {
transform: translateY(-120%);
}
to {
transform: translateY(560%);
}
}
.twr-08:has(#twr-08-theme:checked) {
--color-twr08-bg: #dfe2e3;
--color-twr08-bg: oklch(0.90 0.004 220);
--color-twr08-surface: #f4f6f6;
--color-twr08-surface: oklch(0.97 0.002 220);
--color-twr08-ink: #16191b;
--color-twr08-ink: oklch(0.20 0.005 220);
--color-twr08-ink2: #4a5255;
--color-twr08-ink2: oklch(0.43 0.009 220);
--color-twr08-line: #aab1b4;
--color-twr08-line: oklch(0.75 0.007 220);
--color-twr08-acc: #0d6d5b;
--color-twr08-acc: oklch(0.48 0.1 172);
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The webfonts load as
<link>tags insetup, not as an@importin the stylesheet — Tailwind emits user@importrules after its own layers, where the browser drops them. - This is the collection's reference for JS-plus-utilities: the script sets
data-freshand the look is adata-fresh:variant. No toggled class, no styles hidden from the markup. - Written lines are cloned from a
<template>in the markup, which also guarantees the fresh-state utilities are statically visible to the compiler — a class built in JS would compile to nothing. - Per-tube phosphor data is set as arbitrary custom properties on the tube root, so
duration-[var(--twr-08-decay)]gives each tube its own clock from one shared utility. - Colour and bloom are both in the
data-fresh:set and share one transition, so brightness and glow decay together with no coordinating code. - The retrace is an
--animate-*token withmotion-reduce:animate-nonetyped next to it, and the line transition has amotion-reduce:transition-nonetwin. __twrCleanupon the root is the teardown hook:return () => root.__twrCleanup()in a React effect and the interval never doubles.
Here's a working CSS demo 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: Tailwind Amber and Green Phosphor Monitor
Source: https://codefronts.com/design-styles/tailwind-retro-ui/tailwind-amber-and-green-phosphor-monitor/
Two monochrome tubes side by side with the phosphor named on each: P1 green and P3 amber. Persistence is a real trailing fade on newly written lines rather than a static glow, and the port changes how the script talks to the CSS — it sets a data- attribute and a data-* variant does the styling, which is the pattern to reach for whenever JS and utilities meet.
## HTML
```html
<section class="twr-08 relative block w-full min-h-svh box-border bg-twr08-bg text-twr08-ink font-twr08-mono" aria-labelledby="twr-08-title">
<h2 class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap" id="twr-08-title">Monochrome phosphor monitors, P1 green and P3 amber</h2>
<div class="absolute top-4 right-4 z-6">
<input class="peer absolute inset-0 size-11 m-0 opacity-0 cursor-pointer" type="checkbox" id="twr-08-theme">
<label class="grid place-items-center size-11 rounded-full border border-twr08-line bg-twr08-surface text-twr08-ink2 cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-twr08-acc peer-focus-visible:outline-offset-2" for="twr-08-theme">
<svg class="size-5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"/></svg>
<span class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap">Toggle light surround</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] gap-6 max-w-[74rem] mx-auto px-[clamp(0.85rem,0.5rem+2vw,2rem)] pt-[clamp(3.5rem,3rem+3vw,5rem)] pb-16">
<header class="grid gap-1.5 min-w-0">
<p class="m-0 text-[0.68rem] tracking-[0.14em] uppercase text-twr08-ink2">Monochrome CRT · phosphor persistence and bloom</p>
<h3 class="m-0 min-w-0 text-balance text-[clamp(1.3rem,1rem+1.9vw,2.05rem)] font-semibold leading-tight tracking-[-0.02em]">Two phosphors, one code path</h3>
</header>
<div class="grid grid-cols-[minmax(0,1fr)] gap-4.5 md:grid-cols-2">
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#3c3f3a] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#33ff66] [--twr-08-p-dim:#1c7a3a] [--twr-08-decay:1400ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l1">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p1" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l1">P1 · zinc silicate green</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">long — 1.4 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">525 nm</dd></div>
</dl>
</div>
</article>
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#423d34] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#ffb000] [--twr-08-p-dim:#8a5f00] [--twr-08-decay:600ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l2">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p3" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l2">P3 · zinc beryllium amber</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">medium — 0.6 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">602 nm</dd></div>
</dl>
</div>
</article>
</div>
<template data-twr-08-line>
<p class="m-0 min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-[var(--twr-08-p-dim)] transition-[color,text-shadow] duration-[var(--twr-08-decay)] ease-out motion-reduce:transition-none data-fresh:text-[var(--twr-08-p)] data-fresh:[text-shadow:0_0_3px_var(--twr-08-p),0_0_9px_var(--twr-08-p)]"><span class="inline-block w-[3ch]" data-mark></span><span data-text></span></p>
</template>
<aside class="min-w-0 grid gap-2.5 border border-twr08-line bg-twr08-surface p-[clamp(0.85rem,0.6rem+1vw,1.2rem)]">
<h4 class="m-0 text-[0.72rem] tracking-[0.1em] uppercase text-twr08-ink2">State without colour</h4>
<p class="m-0 min-w-0 max-w-[68ch] text-[0.85rem] leading-relaxed text-pretty text-twr08-ink2">One hue means no colour coding. Every line below marks its state with a bracket glyph in a fixed column and a written word, so the distinction survives on a display that has exactly one colour to give.</p>
<ul class="list-none m-0 p-0 flex flex-wrap gap-x-5 gap-y-1.5 text-[0.8rem]">
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[ ]</span> routine</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[!]</span> warning</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[x]</span> fault</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[>]</span> awaiting input</li>
</ul>
</aside>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-twr08-bg: #1a1c1e;
--color-twr08-bg: oklch(0.23 0.004 220);
--color-twr08-surface: #262a2d;
--color-twr08-surface: oklch(0.30 0.006 220);
--color-twr08-ink: #eef1f2;
--color-twr08-ink: oklch(0.96 0.003 220);
--color-twr08-ink2: #aab2b6;
--color-twr08-ink2: oklch(0.76 0.008 220);
--color-twr08-line: #3e4548;
--color-twr08-line: oklch(0.37 0.008 220);
--color-twr08-acc: #86d7c4;
--color-twr08-acc: oklch(0.84 0.09 172);
--font-twr08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--animate-twr08-retrace: twr-08-retrace 7s linear infinite;
}
@keyframes twr-08-retrace {
from {
transform: translateY(-120%);
}
to {
transform: translateY(560%);
}
}
.twr-08:has(#twr-08-theme:checked) {
--color-twr08-bg: #dfe2e3;
--color-twr08-bg: oklch(0.90 0.004 220);
--color-twr08-surface: #f4f6f6;
--color-twr08-surface: oklch(0.97 0.002 220);
--color-twr08-ink: #16191b;
--color-twr08-ink: oklch(0.20 0.005 220);
--color-twr08-ink2: #4a5255;
--color-twr08-ink2: oklch(0.43 0.009 220);
--color-twr08-line: #aab1b4;
--color-twr08-line: oklch(0.75 0.007 220);
--color-twr08-acc: #0d6d5b;
--color-twr08-acc: oklch(0.48 0.1 172);
}
```
## JavaScript
```js
/* The script sets state only — a data attribute — and never a style or a class.
Appearance lives in data-fresh: variants on the template line. */
(function () {
var root = document.querySelector('.twr-08');
if (!root) return;
var tpl = root.querySelector('[data-twr-08-line]');
var feeds = Array.prototype.slice.call(root.querySelectorAll('[data-twr-08-feed]'));
var lines = [
['[ ]', 'MEM CHECK 640K OK'], ['[ ]', 'SEEK TRK 00 SEC 01'],
['[!]', 'PARITY RETRY 1 OF 3'], ['[ ]', 'BUFFER FLUSHED 4096B'],
['[x]', 'DEVICE 2 NOT READY'], ['[ ]', 'LINK 1200 BAUD 8N1'],
['[>]', 'AWAITING OPERATOR'], ['[ ]', 'SPOOL QUEUE EMPTY']
];
var i = 0;
function write() {
var pair = lines[i % lines.length];
feeds.forEach(function (feed) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.querySelector('[data-mark]').textContent = pair[0];
node.querySelector('[data-text]').textContent = pair[1];
node.setAttribute('data-fresh', '');
feed.appendChild(node);
requestAnimationFrame(function () {
requestAnimationFrame(function () { node.removeAttribute('data-fresh'); });
});
while (feed.childElementCount > 9) feed.removeChild(feed.firstElementChild);
});
i += 1;
}
write();
var timer = setInterval(write, 1500);
root.__twrCleanup = function () {
clearInterval(timer);
feeds.forEach(function (f) { f.textContent = ''; });
};
})();
```Here's a working CSS demo 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: Tailwind Amber and Green Phosphor Monitor
Source: https://codefronts.com/design-styles/tailwind-retro-ui/tailwind-amber-and-green-phosphor-monitor/
Two monochrome tubes side by side with the phosphor named on each: P1 green and P3 amber. Persistence is a real trailing fade on newly written lines rather than a static glow, and the port changes how the script talks to the CSS — it sets a data- attribute and a data-* variant does the styling, which is the pattern to reach for whenever JS and utilities meet.
## HTML
```html
<section class="twr-08 relative block w-full min-h-svh box-border bg-twr08-bg text-twr08-ink font-twr08-mono" aria-labelledby="twr-08-title">
<h2 class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap" id="twr-08-title">Monochrome phosphor monitors, P1 green and P3 amber</h2>
<div class="absolute top-4 right-4 z-6">
<input class="peer absolute inset-0 size-11 m-0 opacity-0 cursor-pointer" type="checkbox" id="twr-08-theme">
<label class="grid place-items-center size-11 rounded-full border border-twr08-line bg-twr08-surface text-twr08-ink2 cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-twr08-acc peer-focus-visible:outline-offset-2" for="twr-08-theme">
<svg class="size-5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"/></svg>
<span class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap">Toggle light surround</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] gap-6 max-w-[74rem] mx-auto px-[clamp(0.85rem,0.5rem+2vw,2rem)] pt-[clamp(3.5rem,3rem+3vw,5rem)] pb-16">
<header class="grid gap-1.5 min-w-0">
<p class="m-0 text-[0.68rem] tracking-[0.14em] uppercase text-twr08-ink2">Monochrome CRT · phosphor persistence and bloom</p>
<h3 class="m-0 min-w-0 text-balance text-[clamp(1.3rem,1rem+1.9vw,2.05rem)] font-semibold leading-tight tracking-[-0.02em]">Two phosphors, one code path</h3>
</header>
<div class="grid grid-cols-[minmax(0,1fr)] gap-4.5 md:grid-cols-2">
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#3c3f3a] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#33ff66] [--twr-08-p-dim:#1c7a3a] [--twr-08-decay:1400ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l1">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p1" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l1">P1 · zinc silicate green</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">long — 1.4 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">525 nm</dd></div>
</dl>
</div>
</article>
<article class="min-w-0 grid gap-2.5 rounded-[10px] bg-[#423d34] p-[clamp(0.6rem,0.4rem+1vw,1rem)] [--twr-08-p:#ffb000] [--twr-08-p-dim:#8a5f00] [--twr-08-decay:600ms] [box-shadow:inset_0_2px_0_rgba(255,255,255,0.22),inset_0_-3px_0_rgba(0,0,0,0.45),0_16px_34px_-18px_rgba(0,0,0,0.7)]" aria-labelledby="twr-08-l2">
<div class="relative isolate overflow-hidden rounded-[14px] min-h-[clamp(200px,34vw,300px)] p-[clamp(0.7rem,0.5rem+1vw,1.1rem)] [background:radial-gradient(120%_100%_at_50%_40%,#0b0d0b_0%,#050605_78%)] [box-shadow:inset_0_0_0_2px_#000,inset_0_0_26px_8px_rgba(0,0,0,0.8)]">
<div class="twr-08__feed min-w-0 grid content-start gap-[0.18em] text-[clamp(0.62rem,0.5rem+0.7vw,0.85rem)] leading-relaxed text-[var(--twr-08-p-dim)]" data-twr-08-feed="p3" role="log" aria-live="off" aria-relevant="additions"></div>
<div class="absolute inset-x-0 h-[22%] z-3 pointer-events-none animate-twr08-retrace motion-reduce:animate-none motion-reduce:opacity-0 [background:linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,0.045)_50%,rgba(255,255,255,0)_100%)]" aria-hidden="true"></div>
</div>
<div class="grid gap-1.5 min-w-0">
<h4 class="m-0 min-w-0 text-[0.78rem] tracking-[0.06em] text-[#e8e6dd]" id="twr-08-l2">P3 · zinc beryllium amber</h4>
<dl class="m-0 flex flex-wrap gap-x-5 gap-y-1 text-[0.7rem] text-[#c3c0b5]">
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Decay</dt><dd class="m-0">medium — 0.6 s visible</dd></div>
<div class="flex gap-1.5 min-w-0"><dt class="m-0 text-[#9c998f]">Peak</dt><dd class="m-0">602 nm</dd></div>
</dl>
</div>
</article>
</div>
<template data-twr-08-line>
<p class="m-0 min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-[var(--twr-08-p-dim)] transition-[color,text-shadow] duration-[var(--twr-08-decay)] ease-out motion-reduce:transition-none data-fresh:text-[var(--twr-08-p)] data-fresh:[text-shadow:0_0_3px_var(--twr-08-p),0_0_9px_var(--twr-08-p)]"><span class="inline-block w-[3ch]" data-mark></span><span data-text></span></p>
</template>
<aside class="min-w-0 grid gap-2.5 border border-twr08-line bg-twr08-surface p-[clamp(0.85rem,0.6rem+1vw,1.2rem)]">
<h4 class="m-0 text-[0.72rem] tracking-[0.1em] uppercase text-twr08-ink2">State without colour</h4>
<p class="m-0 min-w-0 max-w-[68ch] text-[0.85rem] leading-relaxed text-pretty text-twr08-ink2">One hue means no colour coding. Every line below marks its state with a bracket glyph in a fixed column and a written word, so the distinction survives on a display that has exactly one colour to give.</p>
<ul class="list-none m-0 p-0 flex flex-wrap gap-x-5 gap-y-1.5 text-[0.8rem]">
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[ ]</span> routine</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[!]</span> warning</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[x]</span> fault</li>
<li class="flex items-center gap-2 min-w-0"><span class="text-twr08-acc">[>]</span> awaiting input</li>
</ul>
</aside>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-twr08-bg: #1a1c1e;
--color-twr08-bg: oklch(0.23 0.004 220);
--color-twr08-surface: #262a2d;
--color-twr08-surface: oklch(0.30 0.006 220);
--color-twr08-ink: #eef1f2;
--color-twr08-ink: oklch(0.96 0.003 220);
--color-twr08-ink2: #aab2b6;
--color-twr08-ink2: oklch(0.76 0.008 220);
--color-twr08-line: #3e4548;
--color-twr08-line: oklch(0.37 0.008 220);
--color-twr08-acc: #86d7c4;
--color-twr08-acc: oklch(0.84 0.09 172);
--font-twr08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--animate-twr08-retrace: twr-08-retrace 7s linear infinite;
}
@keyframes twr-08-retrace {
from {
transform: translateY(-120%);
}
to {
transform: translateY(560%);
}
}
.twr-08:has(#twr-08-theme:checked) {
--color-twr08-bg: #dfe2e3;
--color-twr08-bg: oklch(0.90 0.004 220);
--color-twr08-surface: #f4f6f6;
--color-twr08-surface: oklch(0.97 0.002 220);
--color-twr08-ink: #16191b;
--color-twr08-ink: oklch(0.20 0.005 220);
--color-twr08-ink2: #4a5255;
--color-twr08-ink2: oklch(0.43 0.009 220);
--color-twr08-line: #aab1b4;
--color-twr08-line: oklch(0.75 0.007 220);
--color-twr08-acc: #0d6d5b;
--color-twr08-acc: oklch(0.48 0.1 172);
}
```
## JavaScript
```js
/* The script sets state only — a data attribute — and never a style or a class.
Appearance lives in data-fresh: variants on the template line. */
(function () {
var root = document.querySelector('.twr-08');
if (!root) return;
var tpl = root.querySelector('[data-twr-08-line]');
var feeds = Array.prototype.slice.call(root.querySelectorAll('[data-twr-08-feed]'));
var lines = [
['[ ]', 'MEM CHECK 640K OK'], ['[ ]', 'SEEK TRK 00 SEC 01'],
['[!]', 'PARITY RETRY 1 OF 3'], ['[ ]', 'BUFFER FLUSHED 4096B'],
['[x]', 'DEVICE 2 NOT READY'], ['[ ]', 'LINK 1200 BAUD 8N1'],
['[>]', 'AWAITING OPERATOR'], ['[ ]', 'SPOOL QUEUE EMPTY']
];
var i = 0;
function write() {
var pair = lines[i % lines.length];
feeds.forEach(function (feed) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.querySelector('[data-mark]').textContent = pair[0];
node.querySelector('[data-text]').textContent = pair[1];
node.setAttribute('data-fresh', '');
feed.appendChild(node);
requestAnimationFrame(function () {
requestAnimationFrame(function () { node.removeAttribute('data-fresh'); });
});
while (feed.childElementCount > 9) feed.removeChild(feed.firstElementChild);
});
i += 1;
}
write();
var timer = setInterval(write, 1500);
root.__twrCleanup = function () {
clearInterval(timer);
feeds.forEach(function (f) { f.textContent = ''; });
};
})();
```How this works
Phosphor type is a specification, not a colour preference. P1 is the zinc-silicate green of early terminals: bright, cheap, noticeably long persistence. P3 amber came later, shorter decay, warmer and slightly dimmer. Reproducing either honestly means different decay times, not just different hues — so both tubes run identical markup and differ only in the custom properties set on their root:
class="[--twr-08-p:#ffb000] [--twr-08-p-dim:#8a5f00] [--twr-08-decay:600ms]"The important port decision is how the script styles things: it does not. The CSS version adds and removes a --fresh modifier class. In a utility codebase that is a trap, because the class the script toggles has no styles of its own and its meaning lives elsewhere. Instead the script sets data-fresh, and the appearance is a variant in the markup:
class="transition-[color,text-shadow] duration-[var(--twr-08-decay)] ease-out
text-[var(--twr-08-p-dim)]
data-fresh:text-[var(--twr-08-p)]
data-fresh:[text-shadow:0_0_3px_var(--twr-08-p),0_0_9px_var(--twr-08-p)]"Now the whole visual contract is visible on the element, the script is three lines shorter, and there is exactly one place to change the decay look. This is the general rule the collection follows for its four scripted demos: JS writes state (data-*, aria-*), never style, never a class that only a stylesheet understands.
Persistence is a transition on write, not a permanent glow. A line starts at full brightness with maximum bloom and decays to the resting level over the phosphor's decay time. That is what a static text-shadow can never capture: it applies the same brightness to text written ten minutes ago as to text written this frame.
Bloom scales with brightness for free. Colour and text-shadow are both in the data-fresh: set and both transition over the same duration, so a fresh line blooms hard and a decayed one barely blooms at all — one state, two properties, no coordination code.
The accessibility limit, stated plainly. A monochrome display has exactly one hue, so error, warning and waiting states cannot be colour-coded. Every line marks its state with a bracket glyph in a fixed column and a written word — never brightness alone, because persistence is already modulating brightness.
Make it yours
- Change
[--twr-08-decay:...]per tube to lengthen or shorten persistence; P1 is authentically slower than P3. - Swap phosphors through
[--twr-08-p:...]on either tube — P4 white and P7 blue-white are two more period-correct options. - Raise the bloom radii inside the
data-fresh:[text-shadow:...]utility; past 6px it stops reading as bloom and starts reading as defocus. - Retime the retrace in the
--animate-twr08-retracetheme entry, or drop the class to remove the artefact without touching the layer. - Change the write cadence by editing the interval constant in the script; nothing in the markup depends on how often lines arrive.
- Drop the second tube for a single-monitor layout — the tubes are independent and carry their own tokens inline.
Gotchas — read before shipping
- Do not have the script toggle a bare class. A class with no styles of its own hides the visual contract in a stylesheet; set
data-*and let adata-fresh:variant carry the look. - Class strings must be statically visible to the compiler, so the fresh-state utilities have to exist in the markup even though only the script activates them — which they do, on the template line.
- A monochrome display cannot carry state in colour. If the only difference between an error and a normal line is brightness, it is invisible, because persistence already varies brightness.
- Amber on black at authentic brightness passes near 6:1, but decayed text drops well below 4.5:1. Treat the decayed state as decoration and keep the resting level above the threshold.
- Never animate
filteror a full-panel blur. Text-shadow on the text column is cheap; a blurred duplicate of the whole screen is not. - The interval must be cleared. Without
__twrCleanupthe tube keeps writing after unmount, and in a ReactuseEffectit doubles on every remount.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
text-shadow, transitions and data-* attribute selectors are all long Baseline, so the phosphor mechanism runs anywhere. Tailwind v4's generated sheet sets the floor at Chrome 111+, Safari 16.4+ and Firefox 128+. The script is plain ES2020 with no optional syntax and runs wherever the CSS does.