Pure TailwindMIT licensed

Tailwind Risograph Print Misregistration Texture

Risograph's charm is its failure mode: a paper feed that never lines up, so every colour lands a few millimetres off and the overlaps print as a third ink. Two offset ink layers, misregistered display type built from attr() pseudo-elements, and paper grain on top — four pixels of error, deliberately.

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="gtw-17 relative block w-full min-h-svh bg-gtw17-paper text-gtw17-ink" aria-labelledby="gtw-17-title">
  <div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
    <input type="checkbox" id="gtw-17-theme" class="gtw-17-theme peer sr-only">
    <label for="gtw-17-theme" class="grid place-items-center w-11 h-11 rounded-none border-2 border-gtw17-ink bg-gtw17-paper text-gtw17-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw17-inkb peer-focus-visible:outline-offset-[3px]">
      <svg viewBox="0 0 24 24" class="w-5 h-5" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="sr-only">Toggle dark theme</span>
    </label>
  </div>

  <main class="w-[min(70rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto pt-[clamp(2rem,6vw,3.5rem)] pb-[clamp(2.5rem,6vw,4rem)] grid gap-[clamp(1.8rem,4.5vw,2.8rem)]">
    <article class="relative isolate overflow-hidden grid items-end min-h-[min(80svh,44rem)] border-2 border-gtw17-ink bg-gtw17-paper">
      <span aria-hidden="true" class="absolute -inset-[8%] pointer-events-none mix-blend-multiply bg-no-repeat opacity-[0.92] translate-x-[calc(var(--gtw17-shift)*-1)] translate-y-[calc(var(--gtw17-shift)*0.6)] bg-[radial-gradient(38%_30%_at_74%_16%,var(--color-gtw17-inka)_0_62%,transparent_64%),radial-gradient(26%_22%_at_18%_76%,var(--color-gtw17-inka)_0_60%,transparent_62%),linear-gradient(178deg,transparent_0_72%,var(--color-gtw17-inka)_72%_80%,transparent_80%)]"></span>
      <span aria-hidden="true" class="absolute -inset-[8%] pointer-events-none mix-blend-multiply bg-no-repeat opacity-[0.88] rotate-[0.3deg] translate-x-[var(--gtw17-shift)] translate-y-[calc(var(--gtw17-shift)*-0.5)] bg-[radial-gradient(34%_27%_at_66%_22%,var(--color-gtw17-inkb)_0_62%,transparent_64%),radial-gradient(30%_24%_at_26%_82%,var(--color-gtw17-inkb)_0_60%,transparent_62%),linear-gradient(178deg,transparent_0_74%,var(--color-gtw17-inkb)_74%_81%,transparent_81%)]"></span>
      <span aria-hidden="true" class="absolute inset-0 pointer-events-none bg-[image:var(--gtw17-noise)] opacity-[var(--gtw17-grain)] mix-blend-multiply"></span>

      <div class="relative z-[3] grid gap-[0.9rem] max-w-[46rem] p-[clamp(1.5rem,5vw,3.2rem)]">
        <p class="m-0 font-gtw17-mono text-[0.74rem] leading-none tracking-[0.2em] uppercase">Bracken Press · Open studio</p>
        <h1 id="gtw-17-title" class="m-0 grid gap-[0.1rem] font-gtw17-display text-[clamp(2.4rem,9vw,5.6rem)] font-normal leading-[0.9] tracking-[-0.03em] uppercase">
          <span data-gtw-17-text="Two Inks" class="relative isolate block text-gtw17-ink before:content-[attr(data-gtw-17-text)] before:absolute before:top-0 before:left-0 before:pointer-events-none before:mix-blend-multiply before:text-gtw17-inka before:translate-x-[calc(var(--gtw17-shift)*-0.7)] before:translate-y-[calc(var(--gtw17-shift)*0.35)] after:content-[attr(data-gtw-17-text)] after:absolute after:top-0 after:left-0 after:pointer-events-none after:mix-blend-multiply after:text-gtw17-inkb after:translate-x-[calc(var(--gtw17-shift)*0.7)] after:translate-y-[calc(var(--gtw17-shift)*-0.35)]">Two Inks</span>
          <span data-gtw-17-text="One Pass" class="relative isolate block text-gtw17-ink before:content-[attr(data-gtw-17-text)] before:absolute before:top-0 before:left-0 before:pointer-events-none before:mix-blend-multiply before:text-gtw17-inka before:translate-x-[calc(var(--gtw17-shift)*-0.7)] before:translate-y-[calc(var(--gtw17-shift)*0.35)] after:content-[attr(data-gtw-17-text)] after:absolute after:top-0 after:left-0 after:pointer-events-none after:mix-blend-multiply after:text-gtw17-inkb after:translate-x-[calc(var(--gtw17-shift)*0.7)] after:translate-y-[calc(var(--gtw17-shift)*-0.35)]">One Pass</span>
          <span data-gtw-17-text="Slightly Wrong" class="relative isolate block text-gtw17-ink before:content-[attr(data-gtw-17-text)] before:absolute before:top-0 before:left-0 before:pointer-events-none before:mix-blend-multiply before:text-gtw17-inka before:translate-x-[calc(var(--gtw17-shift)*-0.7)] before:translate-y-[calc(var(--gtw17-shift)*0.35)] after:content-[attr(data-gtw-17-text)] after:absolute after:top-0 after:left-0 after:pointer-events-none after:mix-blend-multiply after:text-gtw17-inkb after:translate-x-[calc(var(--gtw17-shift)*0.7)] after:translate-y-[calc(var(--gtw17-shift)*-0.35)]">Slightly Wrong</span>
        </h1>
        <p class="m-0 max-w-[32rem] text-[clamp(0.97rem,2vw,1.12rem)] text-pretty">Fourteen printers, one drum, and a paper feed that has never once lined up. Two nights of riso, screen and letterpress in the Sackville Yard workshops.</p>
        <dl class="flex flex-wrap gap-y-2 gap-x-[1.8rem] mt-[0.4rem] mb-0 pt-[0.9rem] border-t-2 border-gtw17-ink">
          <div class="grid gap-[0.1rem]"><dt class="m-0 font-gtw17-mono text-[0.66rem] leading-none tracking-[0.14em] uppercase text-gtw17-mut">When</dt><dd class="m-0 font-gtw17-mono text-[0.98rem] font-medium leading-tight">19–20 June, 6pm</dd></div>
          <div class="grid gap-[0.1rem]"><dt class="m-0 font-gtw17-mono text-[0.66rem] leading-none tracking-[0.14em] uppercase text-gtw17-mut">Where</dt><dd class="m-0 font-gtw17-mono text-[0.98rem] font-medium leading-tight">Unit 7, Sackville Yard</dd></div>
          <div class="grid gap-[0.1rem]"><dt class="m-0 font-gtw17-mono text-[0.66rem] leading-none tracking-[0.14em] uppercase text-gtw17-mut">Entry</dt><dd class="m-0 font-gtw17-mono text-[0.98rem] font-medium leading-tight">Free, bring paper</dd></div>
        </dl>
        <a href="#gtw-17-title" class="justify-self-start inline-flex items-center min-h-[50px] mt-[0.4rem] px-6 border-2 border-gtw17-ink bg-gtw17-inka text-[#14110c] no-underline font-gtw17-display text-[0.9rem] leading-none tracking-[0.06em] uppercase shadow-[var(--gtw17-shift)_var(--gtw17-shift)_0_var(--color-gtw17-inkb)] transition-[translate,box-shadow] duration-150 hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0_var(--color-gtw17-inkb)] focus-visible:outline-2 focus-visible:outline-gtw17-ink focus-visible:outline-offset-[3px] motion-reduce:transition-none">Reserve a press slot</a>
      </div>
    </article>

    <section class="grid gap-[0.8rem] p-[clamp(1.2rem,3vw,1.8rem)] border-2 border-gtw17-ink" aria-labelledby="gtw-17-h-notes">
      <h2 id="gtw-17-h-notes" class="m-0 font-gtw17-display text-[clamp(1.1rem,2.8vw,1.5rem)] font-normal leading-[1.15] tracking-[-0.01em] uppercase">How the misregistration is built</h2>
      <ol class="m-0 pl-[1.4rem] grid gap-2 text-gtw17-mut text-[0.92rem]">
        <li><b class="font-semibold text-gtw17-ink">Duplicate the shape set.</b> Two absolutely positioned layers, identical geometry.</li>
        <li><b class="font-semibold text-gtw17-ink">Offset each by a few pixels.</b> Different directions. 2–5px reads as print; 8px reads as a bug.</li>
        <li><b class="font-semibold text-gtw17-ink">Blend both in multiply.</b> Overlaps darken into a third colour, exactly like overprinted ink.</li>
        <li><b class="font-semibold text-gtw17-ink">Put the grain on top.</b> It is the paper showing through, not dust on the ink.</li>
      </ol>
    </section>
  </main>
</section>
@import "tailwindcss";

@theme {
  --color-gtw17-paper: #f6f1e4;
  --color-gtw17-paper: oklch(0.95 0.02 90);
  --color-gtw17-inka: #ff48a0;
  --color-gtw17-inka: oklch(0.7 0.28 5);
  --color-gtw17-inkb: #1f4bd8;
  --color-gtw17-inkb: oklch(0.48 0.24 265);
  --color-gtw17-ink: #14110c;
  --color-gtw17-ink: oklch(0.2 0.01 85);
  --color-gtw17-mut: #5c5445;
  --color-gtw17-mut: oklch(0.44 0.02 85);
  --font-gtw17-display: 'Archivo Black', 'Anton', 'Helvetica Neue', Impact, sans-serif;
  --font-gtw17-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

.gtw-17 {
  width: 100%;
  min-height: 100svh;
  --gtw17-shift: 4px;
  --gtw17-grain: 0.09;
  --gtw17-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='190' height='190'%3E%3Cfilter id='gtw17n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='190' height='190' filter='url(%23gtw17n)'/%3E%3C/svg%3E");
  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
  line-height: 1.55;
}

.gtw-17:has(.gtw-17-theme:checked) {
  --color-gtw17-paper: #14120d;
  --color-gtw17-paper: oklch(0.18 0.01 90);
  --color-gtw17-ink: #f5efe1;
  --color-gtw17-ink: oklch(0.94 0.02 88);
  --color-gtw17-mut: #a49a86;
  --color-gtw17-mut: oklch(0.7 0.02 85);
  --color-gtw17-inka: #ff5fae;
  --color-gtw17-inka: oklch(0.74 0.26 3);
  --color-gtw17-inkb: #4d79ff;
  --color-gtw17-inkb: oklch(0.62 0.22 268);
  --gtw17-grain: 0.12;
}

@media (prefers-color-scheme: dark) {
  .gtw-17 {
    --color-gtw17-paper: #14120d;
    --color-gtw17-paper: oklch(0.18 0.01 90);
    --color-gtw17-ink: #f5efe1;
    --color-gtw17-ink: oklch(0.94 0.02 88);
    --color-gtw17-mut: #a49a86;
    --color-gtw17-mut: oklch(0.7 0.02 85);
    --color-gtw17-inka: #ff5fae;
    --color-gtw17-inka: oklch(0.74 0.26 3);
    --color-gtw17-inkb: #4d79ff;
    --color-gtw17-inkb: oklch(0.62 0.22 268);
    --gtw17-grain: 0.12;
  }

  .gtw-17:has(.gtw-17-theme:checked) {
    --color-gtw17-paper: #f6f1e4;
    --color-gtw17-paper: oklch(0.95 0.02 90);
    --color-gtw17-ink: #14110c;
    --color-gtw17-ink: oklch(0.2 0.01 85);
    --color-gtw17-mut: #5c5445;
    --color-gtw17-mut: oklch(0.44 0.02 85);
    --color-gtw17-inka: #ff48a0;
    --color-gtw17-inka: oklch(0.7 0.28 5);
    --color-gtw17-inkb: #1f4bd8;
    --color-gtw17-inkb: oklch(0.48 0.24 265);
    --gtw17-grain: 0.09;
  }
}

@media (prefers-color-scheme: light) {
  .gtw-17:has(.gtw-17-theme:checked) {
    --color-gtw17-paper: #14120d;
    --color-gtw17-paper: oklch(0.18 0.01 90);
    --color-gtw17-ink: #f5efe1;
    --color-gtw17-ink: oklch(0.94 0.02 88);
    --color-gtw17-mut: #a49a86;
    --color-gtw17-mut: oklch(0.7 0.02 85);
    --color-gtw17-inka: #ff5fae;
    --color-gtw17-inka: oklch(0.74 0.26 3);
    --color-gtw17-inkb: #4d79ff;
    --color-gtw17-inkb: oklch(0.62 0.22 268);
    --gtw17-grain: 0.12;
  }
}

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-gtw17-paper: #f6f1e4;
  --color-gtw17-paper: oklch(0.95 0.02 90);
  --color-gtw17-inka: #ff48a0;
  --color-gtw17-inka: oklch(0.7 0.28 5);
  --color-gtw17-inkb: #1f4bd8;
  --color-gtw17-inkb: oklch(0.48 0.24 265);
  --color-gtw17-ink: #14110c;
  --color-gtw17-ink: oklch(0.2 0.01 85);
  --color-gtw17-mut: #5c5445;
  --color-gtw17-mut: oklch(0.44 0.02 85);
  --font-gtw17-display: 'Archivo Black', 'Anton', 'Helvetica Neue', Impact, sans-serif;
  --font-gtw17-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

.gtw-17 {
  width: 100%;
  min-height: 100svh;
  --gtw17-shift: 4px;
  --gtw17-grain: 0.09;
  --gtw17-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='190' height='190'%3E%3Cfilter id='gtw17n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='190' height='190' filter='url(%23gtw17n)'/%3E%3C/svg%3E");
  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
  line-height: 1.55;
}

.gtw-17:has(.gtw-17-theme:checked) {
  --color-gtw17-paper: #14120d;
  --color-gtw17-paper: oklch(0.18 0.01 90);
  --color-gtw17-ink: #f5efe1;
  --color-gtw17-ink: oklch(0.94 0.02 88);
  --color-gtw17-mut: #a49a86;
  --color-gtw17-mut: oklch(0.7 0.02 85);
  --color-gtw17-inka: #ff5fae;
  --color-gtw17-inka: oklch(0.74 0.26 3);
  --color-gtw17-inkb: #4d79ff;
  --color-gtw17-inkb: oklch(0.62 0.22 268);
  --gtw17-grain: 0.12;
}

@media (prefers-color-scheme: dark) {
  .gtw-17 {
    --color-gtw17-paper: #14120d;
    --color-gtw17-paper: oklch(0.18 0.01 90);
    --color-gtw17-ink: #f5efe1;
    --color-gtw17-ink: oklch(0.94 0.02 88);
    --color-gtw17-mut: #a49a86;
    --color-gtw17-mut: oklch(0.7 0.02 85);
    --color-gtw17-inka: #ff5fae;
    --color-gtw17-inka: oklch(0.74 0.26 3);
    --color-gtw17-inkb: #4d79ff;
    --color-gtw17-inkb: oklch(0.62 0.22 268);
    --gtw17-grain: 0.12;
  }

  .gtw-17:has(.gtw-17-theme:checked) {
    --color-gtw17-paper: #f6f1e4;
    --color-gtw17-paper: oklch(0.95 0.02 90);
    --color-gtw17-ink: #14110c;
    --color-gtw17-ink: oklch(0.2 0.01 85);
    --color-gtw17-mut: #5c5445;
    --color-gtw17-mut: oklch(0.44 0.02 85);
    --color-gtw17-inka: #ff48a0;
    --color-gtw17-inka: oklch(0.7 0.28 5);
    --color-gtw17-inkb: #1f4bd8;
    --color-gtw17-inkb: oklch(0.48 0.24 265);
    --gtw17-grain: 0.09;
  }
}

@media (prefers-color-scheme: light) {
  .gtw-17:has(.gtw-17-theme:checked) {
    --color-gtw17-paper: #14120d;
    --color-gtw17-paper: oklch(0.18 0.01 90);
    --color-gtw17-ink: #f5efe1;
    --color-gtw17-ink: oklch(0.94 0.02 88);
    --color-gtw17-mut: #a49a86;
    --color-gtw17-mut: oklch(0.7 0.02 85);
    --color-gtw17-inka: #ff5fae;
    --color-gtw17-inka: oklch(0.74 0.26 3);
    --color-gtw17-inkb: #4d79ff;
    --color-gtw17-inkb: oklch(0.62 0.22 268);
    --gtw17-grain: 0.12;
  }
}
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=Archivo+Black&family=JetBrains+Mono:wght@400;500&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • before:content-[attr(data-gtw-17-text)] works exactly as the CSS version did, but now the two ink offsets sit on the element they apply to — so a reader can see that the pink layer goes up-left and the blue goes down-right without cross-referencing a stylesheet.
  • Every offset is derived from --gtw17-shift through calc() inside arbitrary values. That keeps the poster's registration error a single tunable number, which is the honest way to model a press.
  • v4's separate translate and rotate properties mean the skewed ink layer composes rotate-[0.3deg] with its translate without a combined transform string — impossible to express as two utilities in v3.
  • UI change from the CSS original: the muted token dropped from oklch(0.47) to 0.44 for the 0.66rem uppercase labels on cream stock, and the CTA gained a focus-visible: ring — its hard offset shadow made the inherited outline nearly invisible.
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 Risograph Print Misregistration Texture
Source: https://codefronts.com/design-styles/tailwind-css-grain-texture/tailwind-risograph-print-misregistration-texture/

Risograph's charm is its failure mode: a paper feed that never lines up, so every colour lands a few millimetres off and the overlaps print as a third ink. Two offset ink layers, misregistered display type built from attr() pseudo-elements, and paper grain on top — four pixels of error, deliberately.
## HTML
```html
<section class="gtw-17 relative block w-full min-h-svh bg-gtw17-paper text-gtw17-ink" aria-labelledby="gtw-17-title">
  <div class="absolute top-[clamp(1rem,3vw,1.75rem)] right-[clamp(0.85rem,3vw,1.75rem)] z-60">
    <input type="checkbox" id="gtw-17-theme" class="gtw-17-theme peer sr-only">
    <label for="gtw-17-theme" class="grid place-items-center w-11 h-11 rounded-none border-2 border-gtw17-ink bg-gtw17-paper text-gtw17-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-gtw17-inkb peer-focus-visible:outline-offset-[3px]">
      <svg viewBox="0 0 24 24" class="w-5 h-5" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="sr-only">Toggle dark theme</span>
    </label>
  </div>

  <main class="w-[min(70rem,calc(100%-clamp(1.5rem,5vw,4rem)))] mx-auto pt-[clamp(2rem,6vw,3.5rem)] pb-[clamp(2.5rem,6vw,4rem)] grid gap-[clamp(1.8rem,4.5vw,2.8rem)]">
    <article class="relative isolate overflow-hidden grid items-end min-h-[min(80svh,44rem)] border-2 border-gtw17-ink bg-gtw17-paper">
      <span aria-hidden="true" class="absolute -inset-[8%] pointer-events-none mix-blend-multiply bg-no-repeat opacity-[0.92] translate-x-[calc(var(--gtw17-shift)*-1)] translate-y-[calc(var(--gtw17-shift)*0.6)] bg-[radial-gradient(38%_30%_at_74%_16%,var(--color-gtw17-inka)_0_62%,transparent_64%),radial-gradient(26%_22%_at_18%_76%,var(--color-gtw17-inka)_0_60%,transparent_62%),linear-gradient(178deg,transparent_0_72%,var(--color-gtw17-inka)_72%_80%,transparent_80%)]"></span>
      <span aria-hidden="true" class="absolute -inset-[8%] pointer-events-none mix-blend-multiply bg-no-repeat opacity-[0.88] rotate-[0.3deg] translate-x-[var(--gtw17-shift)] translate-y-[calc(var(--gtw17-shift)*-0.5)] bg-[radial-gradient(34%_27%_at_66%_22%,var(--color-gtw17-inkb)_0_62%,transparent_64%),radial-gradient(30%_24%_at_26%_82%,var(--color-gtw17-inkb)_0_60%,transparent_62%),linear-gradient(178deg,transparent_0_74%,var(--color-gtw17-inkb)_74%_81%,transparent_81%)]"></span>
      <span aria-hidden="true" class="absolute inset-0 pointer-events-none bg-[image:var(--gtw17-noise)] opacity-[var(--gtw17-grain)] mix-blend-multiply"></span>

      <div class="relative z-[3] grid gap-[0.9rem] max-w-[46rem] p-[clamp(1.5rem,5vw,3.2rem)]">
        <p class="m-0 font-gtw17-mono text-[0.74rem] leading-none tracking-[0.2em] uppercase">Bracken Press · Open studio</p>
        <h1 id="gtw-17-title" class="m-0 grid gap-[0.1rem] font-gtw17-display text-[clamp(2.4rem,9vw,5.6rem)] font-normal leading-[0.9] tracking-[-0.03em] uppercase">
          <span data-gtw-17-text="Two Inks" class="relative isolate block text-gtw17-ink before:content-[attr(data-gtw-17-text)] before:absolute before:top-0 before:left-0 before:pointer-events-none before:mix-blend-multiply before:text-gtw17-inka before:translate-x-[calc(var(--gtw17-shift)*-0.7)] before:translate-y-[calc(var(--gtw17-shift)*0.35)] after:content-[attr(data-gtw-17-text)] after:absolute after:top-0 after:left-0 after:pointer-events-none after:mix-blend-multiply after:text-gtw17-inkb after:translate-x-[calc(var(--gtw17-shift)*0.7)] after:translate-y-[calc(var(--gtw17-shift)*-0.35)]">Two Inks</span>
          <span data-gtw-17-text="One Pass" class="relative isolate block text-gtw17-ink before:content-[attr(data-gtw-17-text)] before:absolute before:top-0 before:left-0 before:pointer-events-none before:mix-blend-multiply before:text-gtw17-inka before:translate-x-[calc(var(--gtw17-shift)*-0.7)] before:translate-y-[calc(var(--gtw17-shift)*0.35)] after:content-[attr(data-gtw-17-text)] after:absolute after:top-0 after:left-0 after:pointer-events-none after:mix-blend-multiply after:text-gtw17-inkb after:translate-x-[calc(var(--gtw17-shift)*0.7)] after:translate-y-[calc(var(--gtw17-shift)*-0.35)]">One Pass</span>
          <span data-gtw-17-text="Slightly Wrong" class="relative isolate block text-gtw17-ink before:content-[attr(data-gtw-17-text)] before:absolute before:top-0 before:left-0 before:pointer-events-none before:mix-blend-multiply before:text-gtw17-inka before:translate-x-[calc(var(--gtw17-shift)*-0.7)] before:translate-y-[calc(var(--gtw17-shift)*0.35)] after:content-[attr(data-gtw-17-text)] after:absolute after:top-0 after:left-0 after:pointer-events-none after:mix-blend-multiply after:text-gtw17-inkb after:translate-x-[calc(var(--gtw17-shift)*0.7)] after:translate-y-[calc(var(--gtw17-shift)*-0.35)]">Slightly Wrong</span>
        </h1>
        <p class="m-0 max-w-[32rem] text-[clamp(0.97rem,2vw,1.12rem)] text-pretty">Fourteen printers, one drum, and a paper feed that has never once lined up. Two nights of riso, screen and letterpress in the Sackville Yard workshops.</p>
        <dl class="flex flex-wrap gap-y-2 gap-x-[1.8rem] mt-[0.4rem] mb-0 pt-[0.9rem] border-t-2 border-gtw17-ink">
          <div class="grid gap-[0.1rem]"><dt class="m-0 font-gtw17-mono text-[0.66rem] leading-none tracking-[0.14em] uppercase text-gtw17-mut">When</dt><dd class="m-0 font-gtw17-mono text-[0.98rem] font-medium leading-tight">19–20 June, 6pm</dd></div>
          <div class="grid gap-[0.1rem]"><dt class="m-0 font-gtw17-mono text-[0.66rem] leading-none tracking-[0.14em] uppercase text-gtw17-mut">Where</dt><dd class="m-0 font-gtw17-mono text-[0.98rem] font-medium leading-tight">Unit 7, Sackville Yard</dd></div>
          <div class="grid gap-[0.1rem]"><dt class="m-0 font-gtw17-mono text-[0.66rem] leading-none tracking-[0.14em] uppercase text-gtw17-mut">Entry</dt><dd class="m-0 font-gtw17-mono text-[0.98rem] font-medium leading-tight">Free, bring paper</dd></div>
        </dl>
        <a href="#gtw-17-title" class="justify-self-start inline-flex items-center min-h-[50px] mt-[0.4rem] px-6 border-2 border-gtw17-ink bg-gtw17-inka text-[#14110c] no-underline font-gtw17-display text-[0.9rem] leading-none tracking-[0.06em] uppercase shadow-[var(--gtw17-shift)_var(--gtw17-shift)_0_var(--color-gtw17-inkb)] transition-[translate,box-shadow] duration-150 hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0_var(--color-gtw17-inkb)] focus-visible:outline-2 focus-visible:outline-gtw17-ink focus-visible:outline-offset-[3px] motion-reduce:transition-none">Reserve a press slot</a>
      </div>
    </article>

    <section class="grid gap-[0.8rem] p-[clamp(1.2rem,3vw,1.8rem)] border-2 border-gtw17-ink" aria-labelledby="gtw-17-h-notes">
      <h2 id="gtw-17-h-notes" class="m-0 font-gtw17-display text-[clamp(1.1rem,2.8vw,1.5rem)] font-normal leading-[1.15] tracking-[-0.01em] uppercase">How the misregistration is built</h2>
      <ol class="m-0 pl-[1.4rem] grid gap-2 text-gtw17-mut text-[0.92rem]">
        <li><b class="font-semibold text-gtw17-ink">Duplicate the shape set.</b> Two absolutely positioned layers, identical geometry.</li>
        <li><b class="font-semibold text-gtw17-ink">Offset each by a few pixels.</b> Different directions. 2–5px reads as print; 8px reads as a bug.</li>
        <li><b class="font-semibold text-gtw17-ink">Blend both in multiply.</b> Overlaps darken into a third colour, exactly like overprinted ink.</li>
        <li><b class="font-semibold text-gtw17-ink">Put the grain on top.</b> It is the paper showing through, not dust on the ink.</li>
      </ol>
    </section>
  </main>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-gtw17-paper: #f6f1e4;
  --color-gtw17-paper: oklch(0.95 0.02 90);
  --color-gtw17-inka: #ff48a0;
  --color-gtw17-inka: oklch(0.7 0.28 5);
  --color-gtw17-inkb: #1f4bd8;
  --color-gtw17-inkb: oklch(0.48 0.24 265);
  --color-gtw17-ink: #14110c;
  --color-gtw17-ink: oklch(0.2 0.01 85);
  --color-gtw17-mut: #5c5445;
  --color-gtw17-mut: oklch(0.44 0.02 85);
  --font-gtw17-display: 'Archivo Black', 'Anton', 'Helvetica Neue', Impact, sans-serif;
  --font-gtw17-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

.gtw-17 {
  width: 100%;
  min-height: 100svh;
  --gtw17-shift: 4px;
  --gtw17-grain: 0.09;
  --gtw17-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='190' height='190'%3E%3Cfilter id='gtw17n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='190' height='190' filter='url(%23gtw17n)'/%3E%3C/svg%3E");
  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
  line-height: 1.55;
}

.gtw-17:has(.gtw-17-theme:checked) {
  --color-gtw17-paper: #14120d;
  --color-gtw17-paper: oklch(0.18 0.01 90);
  --color-gtw17-ink: #f5efe1;
  --color-gtw17-ink: oklch(0.94 0.02 88);
  --color-gtw17-mut: #a49a86;
  --color-gtw17-mut: oklch(0.7 0.02 85);
  --color-gtw17-inka: #ff5fae;
  --color-gtw17-inka: oklch(0.74 0.26 3);
  --color-gtw17-inkb: #4d79ff;
  --color-gtw17-inkb: oklch(0.62 0.22 268);
  --gtw17-grain: 0.12;
}

@media (prefers-color-scheme: dark) {
  .gtw-17 {
    --color-gtw17-paper: #14120d;
    --color-gtw17-paper: oklch(0.18 0.01 90);
    --color-gtw17-ink: #f5efe1;
    --color-gtw17-ink: oklch(0.94 0.02 88);
    --color-gtw17-mut: #a49a86;
    --color-gtw17-mut: oklch(0.7 0.02 85);
    --color-gtw17-inka: #ff5fae;
    --color-gtw17-inka: oklch(0.74 0.26 3);
    --color-gtw17-inkb: #4d79ff;
    --color-gtw17-inkb: oklch(0.62 0.22 268);
    --gtw17-grain: 0.12;
  }

  .gtw-17:has(.gtw-17-theme:checked) {
    --color-gtw17-paper: #f6f1e4;
    --color-gtw17-paper: oklch(0.95 0.02 90);
    --color-gtw17-ink: #14110c;
    --color-gtw17-ink: oklch(0.2 0.01 85);
    --color-gtw17-mut: #5c5445;
    --color-gtw17-mut: oklch(0.44 0.02 85);
    --color-gtw17-inka: #ff48a0;
    --color-gtw17-inka: oklch(0.7 0.28 5);
    --color-gtw17-inkb: #1f4bd8;
    --color-gtw17-inkb: oklch(0.48 0.24 265);
    --gtw17-grain: 0.09;
  }
}

@media (prefers-color-scheme: light) {
  .gtw-17:has(.gtw-17-theme:checked) {
    --color-gtw17-paper: #14120d;
    --color-gtw17-paper: oklch(0.18 0.01 90);
    --color-gtw17-ink: #f5efe1;
    --color-gtw17-ink: oklch(0.94 0.02 88);
    --color-gtw17-mut: #a49a86;
    --color-gtw17-mut: oklch(0.7 0.02 85);
    --color-gtw17-inka: #ff5fae;
    --color-gtw17-inka: oklch(0.74 0.26 3);
    --color-gtw17-inkb: #4d79ff;
    --color-gtw17-inkb: oklch(0.62 0.22 268);
    --gtw17-grain: 0.12;
  }
}
```

How this works

Misregistration is duplication plus offset plus multiply. The ink layers are identical geometry (three radial blobs and a band) painted in two colours, shifted a few pixels in opposite directions. multiply is mandatory: where the two inks overlap they must darken into a third colour, exactly as overprinted ink does. With normal blending, one layer simply covers the other and the effect looks like a broken duplicate.

The type trick is attr(). Each headline line carries its own text in a data attribute; before: and after: read it back and paint it in the two inks, offset by ±0.7 of the shift. One DOM node, three impressions, and screen readers hear the word once because pseudo-element content is not in the accessibility tree.

Four pixels is the whole range. 2–5px reads as print. 8px reads as a CSS bug. The demo sets --gtw17-shift: 4px and derives every other offset from it by multiplication, which is why the poster stays internally consistent at any value.

Grain goes on last. It is the paper showing through the ink, so it sits above both layers in multiply. Put it underneath and the poster reads as dust on the ink rather than as stock.

Make it yours

  • Registration error: --gtw17-shift. Everything on the page — ink layers, type, button shadow — is derived from it.
  • Inks: --color-gtw17-inka / inkb. Riso palettes are famously saturated; keep both at high chroma or the multiply overlap goes muddy.
  • Skew: rotate-[0.3deg] on the second layer. Above about 1deg the poster reads as a mistake rather than a press.
  • Grain: --gtw17-grain (0.09 light / 0.12 dark). Riso stock is rough — this is one of the few places to go above 0.1.
  • Headline: each line is its own <span> with matching text in data-gtw-17-text. Keep them identical or the inks print different words.

Gotchas — read before shipping

  • Opaque ink layers make the effect look like a broken duplicate. mix-blend-multiply is mandatory, not stylistic.
  • Misregistered display type is illegible below about 40px. Use the effect on headlines only — the demo's body copy carries no offset at all.
  • content-[attr(data-…)] must reference an attribute on the SAME element as the pseudo-element, and the text must match the visible text exactly, or the inks and the ink-black layer disagree.
  • Offsetting with margin or top reflows; use translate-x/translate-y, which are compositor-only and cannot disturb the layout.
  • Very high chroma oklch() inks fall outside sRGB and clip differently across displays. Check the multiply overlap on a wide-gamut screen and an sRGB one.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The technique needs only mix-blend-mode and transforms, both Baseline since 2016. Wide-gamut oklch() inks need Chrome 111, Safari 15.4 and Firefox 113 — below that the sRGB hex fallback prints a slightly duller but correct poster.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…