Tailwind v4MIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

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 &middot; 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 &middot; 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 &mdash; 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 &middot; 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 &mdash; 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">[&gt;]</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);
}
/* 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.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@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);
}
3Fonts used by this demooptional — skip if you have your own type stack
<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 in setup, not as an @import in the stylesheet — Tailwind emits user @import rules after its own layers, where the browser drops them.
  • This is the collection's reference for JS-plus-utilities: the script sets data-fresh and the look is a data-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 with motion-reduce:animate-none typed next to it, and the line transition has a motion-reduce:transition-none twin.
  • __twrCleanup on the root is the teardown hook: return () => root.__twrCleanup() in a React effect and the interval never doubles.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 &mdash; 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 &middot; 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 &middot; 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 &mdash; 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 &middot; 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 &mdash; 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">[&gt;]</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-retrace theme 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 a data-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 filter or 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 __twrCleanup the tube keeps writing after unmount, and in a React useEffect it doubles on every remount.

Browser support

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Plain CSSno Tailwind surface

HTML & JSno Tailwind involved

Search CodeFronts

Loading…