Split-flap board
CSS + JSA departure board: each character is four half-height leaves, and a change drops the old top half and lands the new bottom half. JS only swaps the letters and restarts the CSS animation.
Paragraphs scroll away on a plane tipped back 55°. The fade into the distance is an overlay on top, because a mask on a 3D ancestor would flatten the scene.
Your edited version
perspective-origin: 50% 100%, instead of the default centre, so everything converges toward a vanishing point above the text rather than the middle of the box.transform-origin: 50% 100%) and tipped away from the viewer with rotateX(55deg).animation-delay: -15s on the second), so the crawl is never empty while one copy finishes and the other is only half way up.transform: translateY(...) animates the text, so the scroll runs on the compositor even though the paragraphs are long.perspective-origin at the bottom edgerotateX(55deg) hinged on the bottomtranslateY loop, two copies half a loop apartgradient overlay instead of mask<div class="crawl">
<div class="plane">
<div class="text">
<h4>Episode 3D</h4>
<p>It is a period of flat design. Rebel stylesheets, striking from a hidden folder, have won their first victory against the evil Canvas Empire.</p>
<p>During the battle, a single rotateX managed to tip an entire paragraph back into the distance, using nothing but perspective and a parent that owns it.</p>
<p>Pursued by heavy JavaScript bundles, the text now scrolls home along its plane, with no script on board at all...</p>
</div>
<div class="text" aria-hidden="true">
<h4>Episode 3D</h4>
<p>It is a period of flat design. Rebel stylesheets, striking from a hidden folder, have won their first victory against the evil Canvas Empire.</p>
<p>During the battle, a single rotateX managed to tip an entire paragraph back into the distance, using nothing but perspective and a parent that owns it.</p>
<p>Pursued by heavy JavaScript bundles, the text now scrolls home along its plane, with no script on board at all...</p>
</div>
</div>
<div class="fade"></div>
</div>
.crawl {
position: fixed;
inset: 0;
overflow: hidden;
background: #05060f;
/* the camera sits at the bottom edge; with the plane tipped back 55deg the text converges on
a vanishing line above the bottom, whatever the box's own size */
perspective: 420px;
perspective-origin: 50% 100%;
}
/* hinged on the bottom edge and tipped away from the viewer; it clips its own flat content
(fine here: nothing 3D lives inside it), so text never passes in front of the hinge */
.plane {
position: absolute;
right: 0;
bottom: 0;
left: 0;
width: min(74%, 310px);
height: 950px;
margin: 0 auto;
overflow: hidden;
transform: rotateX(55deg);
transform-origin: 50% 100%;
}
/* two copies of the text, half a loop apart, so the floor is never empty: each one starts just
below the near edge and leaves completely past the far edge, so both ends stay invisible */
.text {
position: absolute;
top: 0;
right: 0;
left: 0;
margin: 0;
color: #ffb547;
font-size: 17px;
font-weight: 700;
line-height: 1.45;
text-align: justify;
animation: crawl-roll 30s linear infinite;
}
.text + .text {
animation-delay: -15s;
}
.text h4 {
margin: 0 0 0.6em;
font-size: 1.5em;
letter-spacing: 0.08em;
text-align: center;
text-transform: uppercase;
}
.text p {
margin: 0 0 1em;
}
/* the fade is a plain overlay ABOVE the plane: a mask on the plane's ancestor would flatten it */
.fade {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 12% 18%, #fff 0.7px, transparent 1.3px),
radial-gradient(circle at 31% 9%, #cfd6ff 0.7px, transparent 1.3px),
radial-gradient(circle at 55% 22%, #fff 1px, transparent 1.7px),
radial-gradient(circle at 72% 12%, #ffe9c2 0.7px, transparent 1.3px),
radial-gradient(circle at 88% 27%, #fff 0.7px, transparent 1.3px),
radial-gradient(circle at 22% 34%, #cfd6ff 0.7px, transparent 1.3px),
radial-gradient(circle at 93% 6%, #fff 1px, transparent 1.7px),
linear-gradient(to top, transparent 90px, #05060f 185px);
pointer-events: none;
}
@keyframes crawl-roll {
from { transform: translateY(950px); }
to { transform: translateY(-100%); }
}
$space: #05060f;
$len: 950px; // length of the floor the text travels along
// A fill scene, always "night": it is outer space in both themes.
.d-crawl {
position: absolute;
inset: 0;
overflow: hidden; // fine here: this element OWNS the perspective, it is not between it and the plane
background: $space;
// The camera sits at the bottom edge. With the plane tipped back 55deg the text converges on
// a vanishing line 420px * cot(55deg) = 294px above the bottom, whatever the stage size.
perspective: 420px;
perspective-origin: 50% 100%;
// The floor: hinged on the bottom edge and tipped away from the viewer. It clips its own flat
// content (allowed: nothing 3D lives inside it), so text never comes in front of the hinge,
// where it would pass through the camera.
&__plane {
position: absolute;
right: 0;
bottom: 0;
left: 0;
width: min(74%, 310px);
height: $len;
margin: 0 auto;
overflow: hidden;
transform: rotateX(55deg);
transform-origin: 50% 100%;
}
// Two copies of the text, half a loop apart, so the floor is never empty. Each one starts
// just below the near edge and leaves completely past the far edge: both ends are invisible,
// which makes the loop seamless.
&__text {
position: absolute;
top: 0;
right: 0;
left: 0;
margin: 0;
color: #ffb547;
font-size: 17px;
font-weight: 700;
line-height: 1.45;
text-align: justify;
animation: d-crawl-roll 30s linear infinite;
& + & {
animation-delay: -15s;
}
h4 {
margin: 0 0 0.6em;
font-size: 1.5em;
letter-spacing: 0.08em;
text-align: center;
text-transform: uppercase;
}
p {
margin: 0 0 1em;
}
}
// The fade into the distance is a plain overlay ABOVE the plane. A mask or opacity on the
// plane's ancestor would flatten the 3D. Measured from the bottom, like the vanishing line.
&__fade {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 12% 18%, #fff 0.7px, transparent 1.3px),
radial-gradient(circle at 31% 9%, #cfd6ff 0.7px, transparent 1.3px),
radial-gradient(circle at 55% 22%, #fff 1px, transparent 1.7px),
radial-gradient(circle at 72% 12%, #ffe9c2 0.7px, transparent 1.3px),
radial-gradient(circle at 88% 27%, #fff 0.7px, transparent 1.3px),
radial-gradient(circle at 22% 34%, #cfd6ff 0.7px, transparent 1.3px),
radial-gradient(circle at 93% 6%, #fff 1px, transparent 1.7px),
linear-gradient(to top, transparent 90px, $space 185px);
pointer-events: none;
}
}
@keyframes d-crawl-roll {
from {
transform: translateY($len);
}
to {
transform: translateY(-100%);
}
}