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

Acute Angles

The base of the markup consists of two <div> elements with the same content, placed inside another container:

<div id="marquee">
        <span>ONE LOVE ONE HEART</span>
    <div aria-hidden="true">
        <span>ONE LOVE ONE HEART</span>

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:

The elements with changed colors and no overflow:hidden, shown at their starting positions

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.


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 to learn more.
Check out the CodePen demo for this article at