Navigation

v0.0.4
Media

Marquee

A row that passes by without end, and stops when you look closely.

On this page

A marquee is a row that passes by without end: the names of customers under a hero, a line of quotes, a ticker. It has no surface and no edge of its own; both ends fade into whatever is behind it. It runs on one CSS animation.

Marquee

<div class="marquee" role="group" aria-label="Teams that build with Master UI">  <ul class="marquee-track">    <li class="text-lg font-medium fg-text-muted">Northwind</li>    <li class="text-lg font-medium fg-text-muted">Lumen</li>    <li class="text-lg font-medium fg-text-muted">Aperture</li>    <li class="text-lg font-medium fg-text-muted">Kestrel</li>    <li class="text-lg font-medium fg-text-muted">Halcyon</li>    <li class="text-lg font-medium fg-text-muted">Meridian</li>    <li class="text-lg font-medium fg-text-muted">Solstice</li>  </ul>  <ul class="marquee-track" aria-hidden="true">    <li class="text-lg font-medium fg-text-muted">Northwind</li>    <li class="text-lg font-medium fg-text-muted">Lumen</li>    <li class="text-lg font-medium fg-text-muted">Aperture</li>    <li class="text-lg font-medium fg-text-muted">Kestrel</li>    <li class="text-lg font-medium fg-text-muted">Halcyon</li>    <li class="text-lg font-medium fg-text-muted">Meridian</li>    <li class="text-lg font-medium fg-text-muted">Solstice</li>  </ul></div>
Hover the row, or move focus into it, and it stops.

#Two copies of the row

The row is written twice, each copy a marquee-track. Both travel one track's length and start again, so the second copy is always where the first one was and the seam never shows. Mark the second copy aria-hidden="true": it exists for the eye, and a screen reader should hear the row once.

A track is at least as wide as the marquee and spreads its items along itself, so a row of three names still reaches both ends.

#Stopping

The row stops while the pointer is over it or the keyboard focus is inside it, which lets a reader finish a quote or follow a link. That is the pause WCAG asks of anything that moves for more than five seconds beside other content.

With prefers-reduced-motion: reduce nothing travels at all. The second copy is removed, the fade is dropped, and the first track wraps onto as many centered lines as it needs: the same content, standing still.

#Speed, spacing and direction

--marquee-duration is the time one copy takes to pass, 40 seconds by default. A longer row needs a longer time to move at the same pace. --marquee-gap is the space between items and --marquee-fade the length of the fade at each end. marquee-reverse sends the row the other way, for the second of two rows.

Two rows of cards

<div class="marquee" style="--marquee-gap: .75rem; --marquee-duration: 48s" role="group" aria-label="What teams say">  <ul class="marquee-track">    <li class="card card-flat w(280)">      <p>“It reads like the platform, only finished.”</p>      <p class="fg-text-muted text-xs">Mira Okafor, Northwind</p>    </li>    <li class="card card-flat w(280)">      <p>“We deleted four hundred lines of positioning script.”</p>      <p class="fg-text-muted text-xs">Jonas Thal, Lumen</p>    </li>    <li class="card card-flat w(280)">      <p>“The first dark mode we did not have to fix afterwards.”</p>      <p class="fg-text-muted text-xs">Ada Reyes, Kestrel</p>    </li>  </ul>  <ul class="marquee-track" aria-hidden="true">    <li class="card card-flat w(280)">      <p>“It reads like the platform, only finished.”</p>      <p class="fg-text-muted text-xs">Mira Okafor, Northwind</p>    </li>    <li class="card card-flat w(280)">      <p>“We deleted four hundred lines of positioning script.”</p>      <p class="fg-text-muted text-xs">Jonas Thal, Lumen</p>    </li>    <li class="card card-flat w(280)">      <p>“The first dark mode we did not have to fix afterwards.”</p>      <p class="fg-text-muted text-xs">Ada Reyes, Kestrel</p>    </li>  </ul></div><div class="marquee marquee-reverse" style="--marquee-gap: .75rem; --marquee-duration: 56s" role="group" aria-label="More of what teams say">  <ul class="marquee-track">    <li class="card card-flat w(280)">      <p>“Every state is an attribute. Our tests got shorter.”</p>      <p class="fg-text-muted text-xs">Sena Ito, Halcyon</p>    </li>    <li class="card card-flat w(280)">      <p>“The select is still a select. That was the whole pitch.”</p>      <p class="fg-text-muted text-xs">Noor Haddad, Meridian</p>    </li>    <li class="card card-flat w(280)">      <p>“Corners that finally match the icons beside them.”</p>      <p class="fg-text-muted text-xs">Eli Brandt, Solstice</p>    </li>  </ul>  <ul class="marquee-track" aria-hidden="true">    <li class="card card-flat w(280)">      <p>“Every state is an attribute. Our tests got shorter.”</p>      <p class="fg-text-muted text-xs">Sena Ito, Halcyon</p>    </li>    <li class="card card-flat w(280)">      <p>“The select is still a select. That was the whole pitch.”</p>      <p class="fg-text-muted text-xs">Noor Haddad, Meridian</p>    </li>    <li class="card card-flat w(280)">      <p>“Corners that finally match the icons beside them.”</p>      <p class="fg-text-muted text-xs">Eli Brandt, Solstice</p>    </li>  </ul></div>

#When not to use it

Moving text is hard to read and easy to resent. Use a marquee for content a reader may glance at and lose nothing by missing: names, short praise, a decorative ticker. Anything a reader must find, compare or act on belongs in a carousel, which they move themselves, or in a plain row.

#Accessibility

  • Give the marquee role="group" and an aria-label that says what the row is.
  • Hide the second track with aria-hidden="true", and keep links and buttons out of it or give them tabindex="-1", so focus never lands on a copy.
  • Do not put information here that appears nowhere else on the page.

#Classes

ClassDescription
marqueeThe row: it clips, fades both ends with a mask and stops on hover and focus. Give it role="group" and a label
marquee-reverseThe row passes the other way, for the second of two rows. With marquee
marquee-trackOne copy of the row, at least as wide as the marquee; write it twice, the second with aria-hidden="true"

#Custom properties

PropertyDefaultDescription
--marquee-duration40sTime for one copy to pass
--marquee-gap3remSpace between items, and between the copies
--marquee-fade4remLength of the fade at each end

#Source

@master/ui/components/marquee.css
/* * Marquee: a row that passes by without end: the names of customers, a line of quotes, a ticker. * The row is written twice, each copy a track; both travel one track's length and start again, so the seam * never shows. The second copy is aria-hidden: it exists for the eye only. Both ends fade into whatever is * behind the marquee with a mask, so it needs no surface of its own. * It stops while the pointer or the keyboard is on it. With reduced motion nothing travels: the copy is * removed and the first track wraps onto as many centered lines as it needs. */@layer components {  .marquee {    --marquee-duration: 40s;    --marquee-gap: 3rem;    --marquee-fade: 4rem;    display: flex;    gap: var(--marquee-gap);    min-width: 0;    overflow: hidden;    mask-image: linear-gradient(to right, transparent, black var(--marquee-fade), black calc(100% - var(--marquee-fade)), transparent);    @media (hover: hover) {      &:hover .marquee-track {        animation-play-state: paused;      }    }    &:focus-within .marquee-track {      animation-play-state: paused;    }    /* The row passes the other way, for the second of two rows. */    &.marquee-reverse .marquee-track {      animation-direction: reverse;    }    @media (prefers-reduced-motion: reduce) {      mask-image: none;    }  }  /*   * One copy of the row. It is at least as wide as the marquee, with its items spread along it, so a short   * row still reaches both ends.   */  .marquee-track {    display: flex;    flex: none;    align-items: center;    justify-content: space-around;    gap: var(--marquee-gap);    min-width: 100%;    margin: 0;    padding: 0;    list-style: none;    /* Linear: the row moves at one speed, and an eased loop would pause at the seam. */    animation: marquee-travel var(--marquee-duration) linear infinite;    @media (prefers-reduced-motion: reduce) {      flex: 1 1 auto;      flex-wrap: wrap;      justify-content: center;      row-gap: calc(var(--marquee-gap) / 2);      min-width: 0;      animation: none;      &[aria-hidden="true"] {        display: none;      }    }  }}@keyframes marquee-travel {  to {    translate: calc(-100% - var(--marquee-gap)) 0;  }}