Theme
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