Pure CSS Text effects #loop #text #matrix #code #rain

Digital rain in pure CSS

Columns of green code fall through the dark at four layers of depth, each with a bright leading glyph and a tail that dims behind it. One perspective makes the far columns small and slow and the near ones large and fast, so the rain has real depth, in pure CSS.

  • 58 lines of CSS
  • 52 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. A column is one element one character wide (width: 1ch), holding its glyphs as a single word that word-break: break-all breaks after every letter. Forty-eight columns are forty-eight elements, and each falls with one translate3d.
  2. The bright head and the dimming tail are one gradient clipped to the letters, under a second layer clipped to the whole box: background-clip: text, border-box. The top layer is a steady green brightening down the column to a near-white last line, the glyph leading the fall; the bottom one is a black strip, so a near column hides the far ones behind it and its letters are always read on black. The tail dims only as far as a green still 6:1 on black: every glyph is text you can read, not a smear.
  3. Every column sits at a depth of its own, translateZ from 165 units behind the screen to 45 in front, in one perspective. Perspective draws the far ones smaller and nearer the middle, so each column works out --k, how much smaller it is drawn, and multiplies its place across and its fall by it: it lands at the share of the canvas it names and falls from just above the top edge to just below the bottom one, at any depth.
  4. Near columns fall in about 3 seconds and far ones in about 7, so the speeds alone read as depth, and the whole field sways ±6° about the vertical so the near columns slide past the far ones. Each column has its own negative animation-delay, so the rain is already falling on the first frame, and every fall starts and ends out of sight: no seam.
  5. The rain is inset: 0 and a size container, so the fall is counted in cqh (shares of the canvas’ height) and the places in percentages: a tall canvas shows the same rain, not more of it. Its sizes are multiples of one base unit, --u, tied to the canvas, so a glyph is the same share of a card and of a full screen.

Key techniques

  • translate3d fall at each column’s own --z
  • background-clip: text, border-box
  • word-break: break-all, 1ch wide
  • negative animation-delay

Copy-paste code

HTML 52 lines

<div class="rain" role="img" aria-label="Columns of green code falling at different depths, like digital rain">
  <div class="field" aria-hidden="true">
    <i style="--x:-2.1;--z:-158;--t:7.2s;--d:-3.9s">6CA+L.HM~K#~9</i>
    <i style="--x:0.6;--z:24;--t:3.4s;--d:-0.6s">GXMUS8NF</i>
    <i style="--x:2.3;--z:-32;--t:4.2s;--d:-3s">N~4X^S+TS</i>
    <i style="--x:4.6;--z:-90;--t:5.3s;--d:-3s">#WN?R7_|5|*13~A8</i>
    <i style="--x:6.6;--z:-157;--t:6.4s;--d:-5.3s">B?P2Q|Y|3~Q!</i>
    <i style="--x:9;--z:30;--t:3.4s;--d:-2s;--m:-1">5W-!M$X~7~D=?CJ@%PZC</i>
    <i style="--x:10.7;--z:-51;--t:4.9s;--d:-1s">70^R6@%^@^</i>
    <i style="--x:14;--z:-102;--t:5.4s;--d:-4.5s;--m:-1">7S?N+4C:</i>
    <i style="--x:15.2;--z:-142;--t:6.5s;--d:-4.3s">SYZ!-J!*T</i>
    <i style="--x:18.6;--z:17;--t:3.3s;--d:-2.9s">F|M5ZK1SWL=0#</i>
    <i style="--x:20.8;--z:-33;--t:4s;--d:-3s">3AM:CYV=8REZ#9*@</i>
    <i style="--x:22.9;--z:-86;--t:5.3s;--d:-5.1s;--m:-1">7K7E1V^~L@</i>
    <i style="--x:24.9;--z:-164;--t:6.5s;--d:-1.2s">V:.6_~AY+KV1Z</i>
    <i style="--x:26.7;--z:27;--t:3.1s;--d:-1.8s;--m:-1">25G#62Q1$3D2YCM</i>
    <i style="--x:29.5;--z:-39;--t:4.4s;--d:-2.9s;--m:-1">V#6U~Q8:</i>
    <i style="--x:31;--z:-89;--t:5.3s;--d:-2.1s;--m:-1">._4RR!?C9K</i>
    <i style="--x:33.3;--z:-146;--t:6.6s;--d:-6.2s">N?#DQH09</i>
    <i style="--x:35.9;--z:31;--t:3.2s;--d:-1.4s">|19W_1D17DUH~~473</i>
    <i style="--x:37.7;--z:-44;--t:4.2s;--d:-4.1s">:GL=|W5D?5MQ</i>
    <i style="--x:40.5;--z:-109;--t:6.1s;--d:-5.9s;--m:-1">NZ6.XHMK:Z!</i>
    <i style="--x:42.8;--z:-154;--t:6.5s;--d:-1.4s">ASNL3WF^HYNR</i>
    <i style="--x:43.9;--z:22;--t:3.6s;--d:-2s">725KYVUM@=1SH?</i>
    <i style="--x:46;--z:-53;--t:5.1s;--d:-1.8s">2+1FS0?P+8AYTU</i>
    <i style="--x:48.6;--z:-95;--t:5.4s;--d:-0.7s">-2:JB_SRJJ7B</i>
    <i style="--x:50.7;--z:-144;--t:5.9s;--d:-4.9s">@~:1F_C8L</i>
    <i style="--x:53;--z:33;--t:3.4s;--d:-2s;--m:-1">5Y+1X^6~1</i>
    <i style="--x:55.2;--z:-40;--t:4.3s;--d:-4.1s;--m:-1">^YTM^@BC$1EF=LL@@</i>
    <i style="--x:58.4;--z:-82;--t:5.5s;--d:-2.9s">=RX7=6H|</i>
    <i style="--x:60;--z:-154;--t:6.5s;--d:-5.2s">+^GH1#S:QV4A14VV*4</i>
    <i style="--x:62.3;--z:39;--t:3.3s;--d:-0.5s;--m:-1">SH!XRBYE0!3|X</i>
    <i style="--x:65;--z:-27;--t:4.4s;--d:-0.6s">WBAL@|SQU%##4U9=0V</i>
    <i style="--x:67.3;--z:-109;--t:5.9s;--d:-5.3s">XU$X:3V#-</i>
    <i style="--x:68.9;--z:-146;--t:7s;--d:-2.7s">M!9C#W#Q?^DMDR</i>
    <i style="--x:71.3;--z:37;--t:3.3s;--d:-0.4s;--m:-1">8J2L$XF1^0HK9F+-*XG</i>
    <i style="--x:72.7;--z:-29;--t:4.3s;--d:-0.3s">0S95Q9P07.L0.H3K</i>
    <i style="--x:75;--z:-83;--t:5.8s;--d:-5.1s;--m:-1">_NJ*JK9B!F</i>
    <i style="--x:77;--z:-152;--t:6.8s;--d:0s">P2?-8=WQ^-?E</i>
    <i style="--x:79.4;--z:21;--t:3.6s;--d:-1.6s">GF8F0H$:RJ@KBJB~5</i>
    <i style="--x:82.7;--z:-49;--t:5.1s;--d:-2.7s">|G4.ST^E8E#A9.X</i>
    <i style="--x:83.7;--z:-87;--t:5.8s;--d:-3.5s;--m:-1">%EL.PU%|!K~Q%9$GE9</i>
    <i style="--x:87.2;--z:-136;--t:5.6s;--d:-3.8s">^ZQJ1TF-</i>
    <i style="--x:89.4;--z:25;--t:3.6s;--d:-2.7s;--m:-1">GKYT5Y735J^LNWM+</i>
    <i style="--x:91.2;--z:-45;--t:4.8s;--d:-0.9s">+BBKJ#~:!FZ8</i>
    <i style="--x:93.1;--z:-81;--t:4.9s;--d:-1.7s">TC95.J!-^%R</i>
    <i style="--x:94.5;--z:-159;--t:7.2s;--d:-4.1s">D:Z0~^74+Y#</i>
    <i style="--x:96.8;--z:39;--t:2.9s;--d:-2s;--m:-1">Z!RZ3|^AT=XMU=:</i>
    <i style="--x:98.9;--z:-26;--t:4.5s;--d:-2.4s">*J*FNC?EQ#Q</i>
    <i style="--x:101.5;--z:-99;--t:5.6s;--d:-1.1s;--m:-1">5D%9M0_Y~#</i>
  </div>
</div>

CSS 58 lines

.rain {
  /* one base unit, tied to the canvas; the rain itself fills the canvas edge to edge, placed in
     percentages of it and falling in shares of its height */
  --u: 0.5vmin;
  position: fixed;
  inset: 0;
  overflow: hidden;
  container-type: size; /* inside it, 1cqh is 1% of the canvas's height */
  perspective: calc(200 * var(--u));
  background: #000;
}

/* the whole field sways a little about the vertical, so near columns slide past far ones */
.field {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: sway 24s ease-in-out infinite;
}

/* a column: a black strip one character wide, its glyphs one per line. --x is where it lands, in
   % of the canvas width; --z its depth in units; --k how much smaller perspective draws it there */
.field i {
  --k: calc((200 - var(--z)) / 200);
  position: absolute;
  top: 0;
  /* perspective pulls a far column towards the middle by --k, so it is placed --k further out */
  left: calc(50% + (var(--x) - 50) * var(--k) * 1%);
  width: 1ch;
  font: 600 calc(10 * var(--u)) / 1.12 'JetBrains Mono', ui-monospace, monospace;
  font-style: normal;
  text-align: center;
  word-break: break-all; /* one character wide, so every letter starts a line */
  /* two layers, each with its own clip: a green that brightens down the column to a near-white
     head, clipped to the letters, over a black strip that fills the box and hides whatever is
     behind it, so the glyphs are always read on black. The darkest green, #14a64c, is 6:1 on it */
  background:
    linear-gradient(#14a64c, #22c55e 50%, #74ffa6 calc(100% - 1.12em), #e9fff1 calc(100% - 1.12em)),
    linear-gradient(#000, #000);
  -webkit-background-clip: text, border-box;
  background-clip: text, border-box;
  color: transparent;
  animation: fall var(--t) linear var(--d) infinite;
}

/* from just above the top edge to just below the bottom one, as perspective draws it at --z:
   50cqh is the vanishing point, and a column drawn --k smaller has --k as far to go each way.
   --m: -1 mirrors a column's glyphs, for the look of the film */
@keyframes fall {
  from { transform: translate3d(-50%, calc(50cqh * (1 - var(--k)) - 100% - 6cqh * var(--k)), calc(var(--z) * var(--u))) scaleX(var(--m, 1)); }
  to   { transform: translate3d(-50%, calc(50cqh * (1 + var(--k)) + 6cqh * var(--k)), calc(var(--z) * var(--u))) scaleX(var(--m, 1)); }
}

/* symmetrical easing, and the end is the start: no seam */
@keyframes sway {
  0%, 100% { transform: rotateY(-6deg); }
  50%      { transform: rotateY(6deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.