For a long time I’ve been interested in making a text crawl for web pages where the text moved around corners. While there are several ways of accomplishing this - WebGL among them - I wanted a solution that would be fairly straightforward, one that could be written using just HTML and CSS.
Acute Angles
The base of the markup consists of two <div>
elements with the same content, placed inside another container:
<div id="marquee">
<div>
<span>ONE LOVE ONE HEART</span>
</div>
<div aria-hidden="true">
<span>ONE LOVE ONE HEART</span>
</div>
</div>
The second element, being a copy of the first, is provided with aria-hidden="true"
to hide it from accessibility applications, so the text is only read once.
The inner <div>
elements are placed at angle to each other by using 3D rotation, with an appropriate perspective
value placed on the parent element. That parent element also has a font-size
of 0
, to ensure that the inline-block
display of the inner elements appears without any gaps:
#marquee {
perspective: 500px;
font-size: 0;
}
#marquee div {
display: inline-block;
height: 12rem;
width: 30rem;
position: relative;
}
The inner elements are bent to slightly different angles using transform-origin
. They are also given different backgrounds (to simulate light coming from one direction) and color
values.
#marquee div:first-of-type {
background: #e5233e;
transform-origin: top right;
transform: rotateY(-40deg);
color: #fff;
}
#marquee div:last-of-type {
background: #b31e31;
transform-origin: top left;
transform: rotateY(45deg);
color: #f8c9d9;
}
The text inside the elements is surrounded by <span>
tags, made wide enough to accommodate a reasonable amount of text. The “clip” to make this text disappear at either end of the divs is created by setting overflow
to hidden
:
#marquee div {
font-size: 8rem;
overflow: hidden;
}
#marquee div span {
position: absolute;
width: 400%;
line-height: 1.4;
}
Forward Crawl
The <span>
elements are offset by different amounts to set them at their different positions: the text on the right is moved by 30rem (i.e. the width of the containing <div>
element to take it outside the div’s visible area. The text on the left is moved twice that amount, and provided with a subtle text-shadow
.
#marquee div:first-of-type span {
transform: translateX(60rem);
animation: leftcrawl 14s linear infinite;
text-shadow: 4px 0px 4px rgba(0,0,0,0.3);
}
#marquee div:last-of-type span {
transform: translateX(30rem);
animation: rightcrawl 14s linear infinite;
}
If the parent <div>
elements did not have overflow:hidden
applied, and the text was in different colors, the initial setup would something like this:
Both text strings are animated. Rather than delaying the motion of the left text string, it moves at the same time as the other string, arriving at its corner at the exact same moment:
@keyframes leftcrawl {
to { transform: translateX(-100rem); }
}
@keyframes rightcrawl {
to { transform: translateX(-130rem); }
}
linear
easing is used so that the text does not speed up or slow down during its motion.
Responsiveness
This technique continues to work regardless of whether the following are changed, as you can discover for yourself in the associated CodePen demo:
- text content
perspective
value- angle of the
<div>
elements
However, the code does not work well with reduced viewport widths: the text gets smaller and the perspective becomes more acute, making the marquee difficult to read. So, below 993px wide, we change the presentation to a 2D display, showing just one of the <div>
elements:
@media all and (max-width: 993px) {
#marquee {
perspective: none;
}
#marquee div:last-of-type {
opacity: 0;
height: 0;
}
#marquee div:first-of-type {
width: 80%;
}
}
Conclusion and Improvements
Overall I’m quite pleased with the result, and look forward to using the technique in a variation of my CSS cinegraphs. There are a few improvements that could be made, however, most especially in that annoying duplication of content. JavaScript could duplicate the elements and text node easily enough, of course, but it would be nice to see if pseudo-elements could be used to do the same thing.
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/EyPbQp