Theme
Design your own
On this page (8)
Component Skill — components/text-rotate/component-skill.md

Native basis

A one-line window (height: 1lh; overflow: hidden) around a stack of 2 - 6 lines; a CSS animation picked by the line count (:has()) moves the stack.

Web Platform APIs

lh:has()animation-play-stateprefers-reduced-motion

Classes

.text-rotate

Data attributes

data-direction="down", data-effect="fade", data-align (center, end), data-pause="none"; the duration of one round: --text-rotate-duration (default 10s).

§Default

Three lines in a big headline: each holds for its share of the 10s round, then rolls up to the next; after the last it rolls back to the first. Hover to pause.

§In a sentence

Inline in running text - the window takes the sentence's font and sits on its baseline; each line can have its own color. The window is as wide as its longest line, so the rotating word reads best at the end of a sentence.

§Centered headline

data-align='center' centers shorter lines in the window; emojis are just text.

§Duration

--text-rotate-duration is one full round through all lines (default 10s): here 4s and 16s.

§Direction and fade

data-direction='down' drops each next line in from above (same reading order); data-effect='fade' crossfades in place with a small drift.

§Line height

The window is 1lh - one line of whatever line-height the text has, so a loose line-height gives each line more room. Keep it at 1.2 or more: tighter than the glyphs themselves, the edges of the neighbouring lines peek into the window.

§Six lines, never pausing

Up to six lines; data-pause='none' keeps it rolling under the pointer (a ticker).

§CSS view file

/* -- Text Rotate component ------------------------------------------ */
@layer components {
  /* daisyUI's text-rotate, CSS only: a .text-rotate holds one <span> of 2-6
     lines (each a child element); one line shows at a time and the stack
     rolls on to the next - an infinite loop that pauses while hovered. The
     window is one line tall (1lh), so it works at any font-size and
     line-height, inline in a sentence or as a headline. The keyframes are
     picked from the line count (:has()); --text-rotate-duration sets one
     full round (default 10s). data-direction="down" rolls the other way,
     data-effect="fade" crossfades in place, data-pause="none" keeps it
     running under the pointer. Reduced motion shows the first line only. */
  .text-rotate {
    --_n: 1;
    --_dur: var(--text-rotate-duration, 10s);
    display: inline-grid;
    height: 1lh;
    overflow: hidden;
    vertical-align: bottom;
    & > span {
      display: flex;
      flex-direction: column;
      animation: var(--_name, none) var(--_dur) cubic-bezier(0.65, 0, 0.35, 1) infinite;
    }
    & > span > * {
      display: block;
      height: 1lh;
      white-space: nowrap;
    }
    &[data-align="center"] > span > * {
      text-align: center;
    }
    &[data-align="end"] > span > * {
      text-align: end;
    }
    &:hover > span,
    &:focus-within > span {
      animation-play-state: paused;
    }
    &[data-pause="none"]:hover > span,
    &[data-pause="none"]:focus-within > span {
      animation-play-state: running;
    }
    &[data-direction="down"] > span {
      flex-direction: column-reverse;
      animation-name: var(--_down, none);
    }
    /* fade: every line in the same cell, each fades through its slot */
    &[data-effect="fade"] > span {
      display: grid;
      animation: none;
    }
    &[data-effect="fade"] > span > * {
      grid-area: 1 / 1;
      opacity: 0;
      animation: var(--_fade, none) var(--_dur) ease infinite both;
    }
    &[data-effect="fade"]:hover > span > *,
    &[data-effect="fade"]:focus-within > span > * {
      animation-play-state: paused;
    }
    &[data-effect="fade"][data-pause="none"]:hover > span > * {
      animation-play-state: running;
    }
  }
  /* the line count picks the keyframes */
  .text-rotate:has(> span > :nth-child(2):last-child) {
    --_n: 2;
    --_name: text-rotate-2;
    --_down: text-rotate-2-down;
    --_fade: text-rotate-fade-2;
  }
  .text-rotate:has(> span > :nth-child(3):last-child) {
    --_n: 3;
    --_name: text-rotate-3;
    --_down: text-rotate-3-down;
    --_fade: text-rotate-fade-3;
  }
  .text-rotate:has(> span > :nth-child(4):last-child) {
    --_n: 4;
    --_name: text-rotate-4;
    --_down: text-rotate-4-down;
    --_fade: text-rotate-fade-4;
  }
  .text-rotate:has(> span > :nth-child(5):last-child) {
    --_n: 5;
    --_name: text-rotate-5;
    --_down: text-rotate-5-down;
    --_fade: text-rotate-fade-5;
  }
  .text-rotate:has(> span > :nth-child(6):last-child) {
    --_n: 6;
    --_name: text-rotate-6;
    --_down: text-rotate-6-down;
    --_fade: text-rotate-fade-6;
  }
  /* fade: line k starts its slot k-1 slots later */
  .text-rotate[data-effect="fade"] > span > :nth-child(2) {
    animation-delay: calc(var(--_dur) * 1 / var(--_n));
  }
  .text-rotate[data-effect="fade"] > span > :nth-child(3) {
    animation-delay: calc(var(--_dur) * 2 / var(--_n));
  }
  .text-rotate[data-effect="fade"] > span > :nth-child(4) {
    animation-delay: calc(var(--_dur) * 3 / var(--_n));
  }
  .text-rotate[data-effect="fade"] > span > :nth-child(5) {
    animation-delay: calc(var(--_dur) * 4 / var(--_n));
  }
  .text-rotate[data-effect="fade"] > span > :nth-child(6) {
    animation-delay: calc(var(--_dur) * 5 / var(--_n));
  }
  @keyframes text-rotate-2 {
    0%,
    46% {
      translate: 0 0;
    }
    50%,
    96% {
      translate: 0 -50%;
    }
    100% {
      translate: 0 0;
    }
  }
  @keyframes text-rotate-2-down {
    0%,
    46% {
      translate: 0 -50%;
    }
    50%,
    96% {
      translate: 0 0;
    }
    100% {
      translate: 0 -50%;
    }
  }
  @keyframes text-rotate-fade-2 {
    0% {
      opacity: 0;
      translate: 0 0.25lh;
    }
    4%,
    46% {
      opacity: 1;
      translate: 0 0;
    }
    50%,
    100% {
      opacity: 0;
      translate: 0 -0.25lh;
    }
  }
  @keyframes text-rotate-3 {
    0%,
    30.667% {
      translate: 0 0;
    }
    33.333%,
    64% {
      translate: 0 -33.3333%;
    }
    66.667%,
    97.333% {
      translate: 0 -66.6667%;
    }
    100% {
      translate: 0 0;
    }
  }
  @keyframes text-rotate-3-down {
    0%,
    30.667% {
      translate: 0 -66.6667%;
    }
    33.333%,
    64% {
      translate: 0 -33.3333%;
    }
    66.667%,
    97.333% {
      translate: 0 0;
    }
    100% {
      translate: 0 -66.6667%;
    }
  }
  @keyframes text-rotate-fade-3 {
    0% {
      opacity: 0;
      translate: 0 0.25lh;
    }
    2.667%,
    30.667% {
      opacity: 1;
      translate: 0 0;
    }
    33.333%,
    100% {
      opacity: 0;
      translate: 0 -0.25lh;
    }
  }
  @keyframes text-rotate-4 {
    0%,
    23% {
      translate: 0 0;
    }
    25%,
    48% {
      translate: 0 -25%;
    }
    50%,
    73% {
      translate: 0 -50%;
    }
    75%,
    98% {
      translate: 0 -75%;
    }
    100% {
      translate: 0 0;
    }
  }
  @keyframes text-rotate-4-down {
    0%,
    23% {
      translate: 0 -75%;
    }
    25%,
    48% {
      translate: 0 -50%;
    }
    50%,
    73% {
      translate: 0 -25%;
    }
    75%,
    98% {
      translate: 0 0;
    }
    100% {
      translate: 0 -75%;
    }
  }
  @keyframes text-rotate-fade-4 {
    0% {
      opacity: 0;
      translate: 0 0.25lh;
    }
    2%,
    23% {
      opacity: 1;
      translate: 0 0;
    }
    25%,
    100% {
      opacity: 0;
      translate: 0 -0.25lh;
    }
  }
  @keyframes text-rotate-5 {
    0%,
    18.4% {
      translate: 0 0;
    }
    20%,
    38.4% {
      translate: 0 -20%;
    }
    40%,
    58.4% {
      translate: 0 -40%;
    }
    60%,
    78.4% {
      translate: 0 -60%;
    }
    80%,
    98.4% {
      translate: 0 -80%;
    }
    100% {
      translate: 0 0;
    }
  }
  @keyframes text-rotate-5-down {
    0%,
    18.4% {
      translate: 0 -80%;
    }
    20%,
    38.4% {
      translate: 0 -60%;
    }
    40%,
    58.4% {
      translate: 0 -40%;
    }
    60%,
    78.4% {
      translate: 0 -20%;
    }
    80%,
    98.4% {
      translate: 0 0;
    }
    100% {
      translate: 0 -80%;
    }
  }
  @keyframes text-rotate-fade-5 {
    0% {
      opacity: 0;
      translate: 0 0.25lh;
    }
    1.6%,
    18.4% {
      opacity: 1;
      translate: 0 0;
    }
    20%,
    100% {
      opacity: 0;
      translate: 0 -0.25lh;
    }
  }
  @keyframes text-rotate-6 {
    0%,
    15.333% {
      translate: 0 0;
    }
    16.667%,
    32% {
      translate: 0 -16.6667%;
    }
    33.333%,
    48.667% {
      translate: 0 -33.3333%;
    }
    50%,
    65.333% {
      translate: 0 -50%;
    }
    66.667%,
    82% {
      translate: 0 -66.6667%;
    }
    83.333%,
    98.667% {
      translate: 0 -83.3333%;
    }
    100% {
      translate: 0 0;
    }
  }
  @keyframes text-rotate-6-down {
    0%,
    15.333% {
      translate: 0 -83.3333%;
    }
    16.667%,
    32% {
      translate: 0 -66.6667%;
    }
    33.333%,
    48.667% {
      translate: 0 -50%;
    }
    50%,
    65.333% {
      translate: 0 -33.3333%;
    }
    66.667%,
    82% {
      translate: 0 -16.6667%;
    }
    83.333%,
    98.667% {
      translate: 0 0;
    }
    100% {
      translate: 0 -83.3333%;
    }
  }
  @keyframes text-rotate-fade-6 {
    0% {
      opacity: 0;
      translate: 0 0.25lh;
    }
    1.333%,
    15.333% {
      opacity: 1;
      translate: 0 0;
    }
    16.667%,
    100% {
      opacity: 0;
      translate: 0 -0.25lh;
    }
  }
  /* -- Accessibility ----------------------------------------------------
     Reduced motion: no rolling - the first line stays. All lines stay in
     the DOM, so assistive tech reads the full list. */
  @media (prefers-reduced-motion: reduce) {
    .text-rotate > span,
    .text-rotate[data-effect="fade"] > span > * {
      animation: none;
    }
    .text-rotate[data-effect="fade"] > span > :first-child {
      opacity: 1;
    }
  }
}

Comments, ideas or improvements? Edit this page's source on GitHub