---
title: "Marquee text"
description: "One line of text that does not fit its place. It rests cut with an ellipsis, and while its trigger holds it scrolls sideways at a steady pace to show all of itself. It moves only when the text overflows, never where motion is reduced, and is read once, whole."
url: "https://tanrei.chnotchy.com/components/marquee-text/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Marquee text

> One line of text that does not fit its place. It rests cut with an ellipsis, and while its trigger holds it scrolls sideways at a steady pace to show all of itself. It moves only when the text overflows, never where motion is reduced, and is read once, whole.

## Overview

A marquee text is one line of the product’s text in a place too narrow for it. At rest it reads as any cut line does: it starts at the line’s start and ends with an ellipsis where it is cut. While its trigger holds — by default, while the pointer is over it or the control it stands in has keyboard focus — it scrolls sideways at a steady pace, given in pixels per second, so that every part of the text passes through view; then it comes back to its start and rests. Point at the first line above. The second fits, so it never moves: a marquee text whose text fits is a plain line of text.

It suits the places where a line has a fixed width and its text is usually short but sometimes is not: the title in a list row, a label in a navigation bar or a drawer, a chip, the name of what is playing, a cell of a table. Where a cut line is enough most of the time, the other choice is to leave it cut and give the full text in a [tooltip](https://tanrei.chnotchy.com/components/tooltip/), which shows it whole at once and moves nothing.

### Usage

The Web and React samples show the current spec. SwiftUI, Compose, Flutter and React Native are planned: they do not ship a marquee text yet.

**Web**

```html
<!-- The default: moves while the pointer is over it or the control it stands in, or while that control has keyboard focus. -->
<tanrei-marquee-text>Track 7 — A very long title that does not fit in the row it stands in</tanrei-marquee-text>

<!-- Moves to its end and back, once for each hover or focus. -->
<tanrei-marquee-text loop="bounce" iterations="1">…</tanrei-marquee-text>

<!-- The name of what is playing: moves by itself, with its own pause button. -->
<tanrei-marquee-text trigger="always" playback-control play-label="Play scrolling text" pause-label="Pause scrolling text">
  Now playing: a very long title of a song by an artist with a long name
</tanrei-marquee-text>

<script type="module">
  import '@tanrei/web/components/marquee-text';
</script>
```

**React**

```tsx
import { MarqueeText } from '@tanrei/react';

<MarqueeText>{track.title}</MarqueeText>

<MarqueeText loop="bounce" iterations={1}>{track.title}</MarqueeText>

{/* Moves by itself; the page's own control that pauses every moving part sets paused. */}
<MarqueeText trigger="always" paused={motionPaused}>
  {`Now playing: ${track.title}`}
</MarqueeText>
```

## Specs

- Category: Communication
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/marquee-text/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/marquee-text.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-marquee-text>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/marquee-text`).
- React: `MarqueeText` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-marquee-text` | alpha |
| SwiftUI | `TanreiMarqueeText` | planned |
| Jetpack Compose | `TanreiMarqueeText` | planned |
| Flutter | `TanreiMarqueeText` | planned |
| React Native | `MarqueeText` | planned |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `paused` (prop) | `paused` | `isPaused` | `paused` | `paused` | `paused` |
| `playbackControl` (prop) | `playbackControl` | `showsPlaybackControl` | `playbackControl` | `playbackControl` | `playbackControl` |
| `pausedChange` (event) | `pausedchange` | `isPaused binding` | `onPausedChange` | `onPausedChanged` | `onPausedChange` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `viewport`: The line's box: as wide as its place, one line high, clipping the text. While the text moves, its clipped edges fade out.
- `text`: The product's text, on one line, in the text style of where it stands. At rest it starts at the line's start and ends with an ellipsis where it is cut.
- `copy` (optional): While the text repeats, a second drawing of it a gap after the first, so that the line never empties. Hidden from assistive technology.
- `playback-control` (optional): While the text scrolls by itself all the time, an icon-only Button of the text variant and the small size at the line's end, pausing and playing the motion.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `trigger` | `enum`: `hover-or-focus`, `hover`, `focus`, `always` | `"hover-or-focus"` | `trigger` | When the text scrolls. hover: while a fine pointer is over the line or the control it stands in. focus: while that control, or something in the line, has keyboard focus. hover-or-focus: either. always: from when the line is shown, by itself, with a way to pause it. |
| `loop` | `enum`: `repeat`, `bounce` | `"repeat"` | `loop` | How a pass goes. repeat: the text moves toward its start until its end has passed, a copy following it a gap behind, so that the line comes back to where it began. bounce: the text moves until its end is in view, pauses, and moves back. |
| `speed` | `number` | — | `speed` | How fast the text moves, in pixels per second, so that a long text and a short one move at the same pace. Null takes comp.marquee-text.speed. |
| `startPause` | `number` | — | `start-pause` | How long the text rests at its start before each pass, in milliseconds; 0 rests none. Null takes comp.marquee-text.motion.start-pause. |
| `endPause` | `number` | — | `end-pause` | While loop is bounce: how long the text rests with its end in view before it moves back, in milliseconds. Null takes comp.marquee-text.motion.end-pause. |
| `iterations` | `number` | — | `iterations` | How many passes a trigger plays before the text rests at its start. Null plays passes without end while the trigger holds: until the pointer leaves or focus moves away for hover, focus and hover-or-focus, and for as long as the line is shown for always. |
| `paused` | `boolean` | `false` | `paused` | Holds the motion where it is: set by the playback control, or by the product's own control that pauses every moving part of the page. |
| `playbackControl` | `boolean` | `false` | `playback-control` | While trigger is always: whether the line draws its playback control. Off by default: a product that moves text all the time gives a way to pause it — this control, or its own control that pauses every moving part of the page and sets paused; without either, a person cannot stop the motion, which WCAG 2.2.2 requires of motion that lasts more than five seconds. |
| `playLabel` | `string` | `""` | `play-label` | The playback control's accessible name while the motion is paused ("Play scrolling text"). Required while the control is drawn. |
| `pauseLabel` | `string` | `""` | `pause-label` | The playback control's accessible name while the text moves ("Pause scrolling text"). Required while the control is drawn. |

### Events

| Event | Description |
| --- | --- |
| `pausedChange` | The person paused or played the motion with the playback control; carries paused. The product that sets paused emits nothing. |

### Slots

| Slot | Description |
| --- | --- |
| `default` | The text: plain text, or inline content of the product's (a bold run, an inline mark). A fixture's string is drawn as plain text. |

### States

`enabled`

### Accessibility

- Role: `none (text)`
- Labeling: The text is read once, whole, as text; the copy is hidden. The playback control is named by pauseLabel and playLabel, from the instance and then the product's words.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab | Reaches the playback control while it is drawn; the line itself is not a stop. Focus on the control the line stands in (a row, a chip) starts the motion for trigger focus and hover-or-focus, and moving focus away stops it. |
| Enter or Space on the playback control | Pauses or plays the motion. |

- Pause, Stop, Hide (WCAG 2.2.2): motion that starts without a person's direct action and lasts more than five seconds beside other content needs a way to pause, stop or hide it, and the Understanding document counts motion started by a hover, a focus or scrolling into view, and stopped only while focused, as not meeting that. The defaults can conflict with it: trigger always draws no playback control (playbackControl is false), and a hover or a focus plays passes until it ends (iterations is null). A product meets it by drawing the playback control or binding its page-wide pause control to paused for trigger always, and by setting iterations so that the motion a hover or a focus starts ends within five seconds, or by keeping such lines where nothing else is read beside them.
- Animation from interactions (WCAG 2.3.3): where motion is reduced, the line never moves; the hidden part is still shown, a viewport at a time, without a motion (MRQ-8).
- The minimum target is the playback control's, a Button's; the line takes no input of its own.

### Behaviour

Requirements in EARS form, each with its stable id.

- **MRQ-1** THE marquee text SHALL lay its content out on one line, in the text style, color and direction of where it stands, in a viewport as wide as its place and one line high, and SHALL clip it there; WHILE the content fits, THE marquee text SHALL draw it as plain text, start nothing, draw no copy and no playback control, and be in every way a line of text.
- **MRQ-2** WHILE the content does not fit and does not move THE marquee text SHALL rest it with its start at the viewport's start, cut at the viewport's end with an ellipsis of the platform's text, so that the line reads as any cut line does.
- **MRQ-3** WHILE its trigger holds — trigger hover: a fine pointer is over the line or over the nearest element around it that takes presses or focus; focus: that element, or something inside the line, has keyboard focus; hover-or-focus: either; always: the line is shown and in view — and the content does not fit, THE marquee text SHALL wait comp.marquee-text.motion.start-delay (for always, startPause) and then play its passes (MRQ-4, MRQ-5): iterations passes where iterations is not null, and without end while the trigger holds where it is null; after the last pass it SHALL rest as MRQ-2 says. WHEN the trigger ends before the passes do THE marquee text SHALL return the content to its start over comp.marquee-text.motion.return-duration with comp.marquee-text.motion.return-easing and rest; a new trigger SHALL play its passes again from the start.
- **MRQ-4** WHILE loop is repeat THE marquee text SHALL play a pass by moving the content toward its start edge at speed (comp.marquee-text.speed while speed is null) in pixels per second, steadily, with comp.marquee-text.motion.easing, a copy of the content following it comp.marquee-text.gap behind, until the copy stands where the content began — then the line looks as it did — and SHALL rest there startPause (comp.marquee-text.motion.start-pause while it is null) before the next pass.
- **MRQ-5** WHILE loop is bounce THE marquee text SHALL play a pass by moving the content toward its start edge at speed until its end stands at the viewport's end, resting there endPause (comp.marquee-text.motion.end-pause while it is null), moving it back at the same speed until its start stands at the viewport's start, and resting there startPause before the next pass; it SHALL draw no copy.
- **MRQ-6** WHILE the marquee text plays its passes — moving, or resting between two passes — THE marquee text SHALL draw no ellipsis and SHALL fade each edge of the viewport behind which content lies hidden over comp.marquee-text.fade.extent, from clear to the content's full color; WHEN it rests after its passes, or because its trigger ended, it SHALL draw the ellipsis of MRQ-2 again and no fade.
- **MRQ-7** THE marquee text SHALL move the content in the direction its text is written: toward the left for text whose direction is left to right and toward the right for text whose direction is right to left, the direction being the content's own — the base direction of its first strong character, or the layout direction where it has none — so that the line always moves to bring the text's hidden end into view; it SHALL rest it at the start edge of that direction, cut at the other.
- **MRQ-8** WHERE motion is reduced THE marquee text SHALL NOT move its content; WHILE a trigger other than always holds it SHALL instead show the hidden part at once, without a motion, by placing the content so that the next viewport's width of it lies in view, the last view ending at the content's end, with the fade of MRQ-6 at each edge behind which content lies hidden, resting startPause on each such view and returning to the start at once after the last, so that every part of the text can be seen; it SHALL go through the views iterations times, or without end while the trigger holds where iterations is null. WHILE trigger is always it SHALL rest as MRQ-2 says and draw no playback control.
- **MRQ-9** WHILE trigger is always and the content moves THE marquee text SHALL hold its motion where it is while a pointer is over the line or focus is within it, and go on from there when neither is; WHILE paused is true it SHALL hold its motion where it is until paused is false; WHILE the line is out of view or its page is hidden it SHALL hold its motion and start the pass again from the start when it is back. Holding on hover and focus SHALL NOT be taken as the way to stop the motion: that is MRQ-10.
- **MRQ-10** WHILE trigger is always, playbackControl is true, the content does not fit and motion is not reduced, THE marquee text SHALL draw at the end of its line, comp.marquee-text.playback.gap after the viewport, an icon-only Button of the text variant and the small size, showing the icon set's pause glyph while the text moves and play_arrow while it is paused, named by pauseLabel while it moves and playLabel while it is paused; WHEN the person activates it THE marquee text SHALL toggle paused and emit pausedChange once, keeping focus on the button. WHILE trigger is always, the content does not fit, motion is not reduced and playbackControl is false, THE marquee text SHALL in debug builds report once that the product must give the page a control that sets paused, or set playbackControl (WCAG 2.2.2).
- **MRQ-11** THE marquee text SHALL expose its content to assistive technology once, whole, as the text it is, read where the line stands, and SHALL hide the copy, the ellipsis and the fade from it; the motion SHALL change nothing it exposes, SHALL make no announcement, and SHALL take no focus: the line itself is not a focus stop.
- **MRQ-12** WHILE playbackControl would be drawn and playLabel or pauseLabel is empty THE marquee text SHALL read the missing name from the product's words, still draw the control where neither gives it, and in debug builds report that it has no accessible name; THE marquee text SHALL have no words of its own.
- **MRQ-13** WHILE the text size is enlarged THE marquee text SHALL grow its line with the text and move it at speed times the ratio of its text's size to the size of that text style at the default text size, so that as many characters pass in a second at every text size; the fade and the gap SHALL keep their sizes.
- **MRQ-14** WHEN the content, the viewport's width or the text size changes THE marquee text SHALL measure again whether the content fits, and WHILE a pass is playing SHALL end it at once and start again from the start where it still does not fit, or rest as MRQ-1 says where it now does; WHEN speed, startPause, endPause, loop or trigger changes it SHALL take the new value from the next pass.
- **MRQ-15** IF speed is not a positive number, or startPause, endPause or iterations is negative, THE marquee text SHALL treat it as null and in debug builds report it; WHEN trigger or loop names a value the component does not define THE marquee text SHALL use the default value (hover-or-focus, repeat) and in debug builds report the unknown value.
- **MRQ-16** WHERE the layout direction is right-to-left THE marquee text SHALL place its playback control at the left end of the line, and the content SHALL move and rest by its own direction as MRQ-7 says, whatever the layout direction.
- **MRQ-17** THE marquee text SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the line is shown THE marquee text SHALL rest at once and draw itself for the new direction.
- **MRQ-18** WHERE the platform draws with a person's own forced colors THE marquee text SHALL draw its text in the color the platform gives the element around it, and its fade SHALL be no fade: the content is clipped at the viewport's edge.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.marquee-text.…` → `--tanrei-comp-marquee-text-…`).

#### `tanrei.comp.marquee-text`

| Token | Type | Value |
| --- | --- | --- |
| `comp.marquee-text.speed` | `number` | `30` |
| `comp.marquee-text.gap` | `dimension` | `{tanrei.sys.space.space600}` |
| `comp.marquee-text.fade.extent` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.marquee-text.playback.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.marquee-text.motion.start-pause` | `duration` | `1200ms` |
| `comp.marquee-text.motion.end-pause` | `duration` | `1200ms` |
| `comp.marquee-text.motion.start-delay` | `duration` | `{tanrei.sys.motion.role.show-delay}` |
| `comp.marquee-text.motion.easing` | `cubicBezier` | `{tanrei.sys.motion.easing.linear}` |
| `comp.marquee-text.motion.return-duration` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.marquee-text.motion.return-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |

## Guidelines

### When to use

Use a marquee text where a line must stay one line, at a width the layout fixes, and a person sometimes needs the part that is cut: a long song title in a row of a playlist, a destination’s label that a language makes long, a chip that holds a name. The line keeps its height and its place, and shows the rest of itself only when asked.

Don’t use it for text a person must read in full to act — a warning, an error, the terms of a choice, an amount, an instruction. Moving text is slow to read and easy to miss. Give such text the lines it needs, and let it wrap.

### Choosing a trigger

| Trigger | Use for |
| --- | --- |
| `hover-or-focus` | The default, and the right choice almost everywhere: the line moves when a person points at it or reaches the control it stands in with the keyboard, so both see the same thing. |
| `hover` | A long list that keyboard users move through quickly, where every row’s title starting to move as focus passes would distract more than it helps. |
| `focus` | A line in a surface used mostly with the keyboard or a remote control, where hovering means nothing. |
| `always` | The name of what is playing, and lines like it that a person glances at rather than points at. It moves by itself from when it is shown, and needs a way to pause it (see Accessibility). |

The trigger’s area is the control the line stands in — a row, a chip, a destination — not only the line: pointing anywhere at the row moves its title. Hover counts only a fine pointer, such as a mouse or a pen; a touch does not start the motion.

### Repeat or bounce

- **`repeat`** (the default) moves the text toward its start until its end has passed, with a copy following it a gap behind, so that the line never empties and comes back to where it began. It reads like a ticker, and suits a line that moves for a while.
- **`bounce`** moves the text until its end is in view, rests there, and moves it back. It draws no copy, so the end and the start are never seen side by side. It suits a line that moves once, for a short look at its end.

Both move the text in the direction it is written: toward the left for a left-to-right text, toward the right for a right-to-left one, whatever the page around it.

### Speed and pauses

- **`speed`** is in pixels per second, not a time for a whole pass, so a long title and a short one move at the same pace. Leave it to the token, `comp.marquee-text.speed`, so that every moving line of the product keeps one pace. When the text size is enlarged, the speed grows with the text, and as many characters pass in a second at every size.
- **`startPause`** is how long the text rests at its start before each pass, so that its beginning can be read. With `trigger="always"` it is also the wait before the first pass.
- **`endPause`**, with `bounce`, is how long the text rests with its end in view before it moves back.
- **`iterations`** is how many passes one trigger plays before the text rests at its start. Left at null, passes go on until the pointer leaves or focus moves away, and for `always`, for as long as the line is shown. A count that keeps the motion short is what makes a hover or focus trigger accessible (below).

The pauses are reading times. The product’s motion settings, which speed animation up or slow it down, do not shorten them.

### Content

Keep the text short enough that people rarely need to see it move: the marquee is for the occasional title that is longer than the rest, not a way to fit a sentence into a chip. Put what tells items apart at the start, where it is read without motion. The text may hold inline content, such as a bold run; it is always one line.

### Do

- Let a line stand in the control a person points at or focuses, so that the trigger’s area is the whole row.
- Set `iterations` so that each hover or focus plays a short run and stops.
- Give a line with `trigger="always"` a way to pause it: its own playback control, or the product’s control that pauses every moving part of the page.

### Don’t

- Don’t use it for text a person must read in full to act. Give that text its own lines.
- Don’t put several lines that move all the time on one screen. One moving line draws the eye; several compete with each other and with everything else on the screen.
- Don’t make the line itself a button or a link to start the motion. It is text; the control around it is what a person presses.
- Don’t shorten the pauses to fit a long text into a short run. Shorten the text, or show it whole another way.

## Accessibility

### The defaults can conflict with WCAG 2.2.2

[Pause, Stop, Hide](https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html) (WCAG 2.2.2, Level A) asks that motion which starts by itself, lasts more than five seconds and stands beside other content can be paused, stopped or hidden by the person. Tanrei’s defaults do not meet it on their own, in two ways, and the product decides how to meet it.

1. **`trigger="always"` draws no playback control by default** (`playbackControl` is false). A line that moves all the time for more than five seconds beside other content needs a way to pause it. Either set `playbackControl`, and the line draws an icon-only pause and play button at its end, named by `pauseLabel` and `playLabel` or by the product’s words `marqueePauseLabel` and `marqueePlayLabel`; or bind the product’s own control that pauses every moving part of the page to `paused`. A debug build reports a line that moves all the time with neither.
2. **With `hover`, `focus` and `hover-or-focus`, `iterations` at null plays passes until the pointer leaves or focus moves away.** The WCAG Understanding document counts motion started by a hover or a focus as motion that starts automatically, and says that motion which stops only when focus leaves is not a means to stop it. To meet 2.2.2, set `iterations` so that the motion ends within five seconds. For a long text, one pass may already take longer: shorten the text or raise the speed, or show the full text another way.

The line holds its motion while a pointer is over it or focus is within it, and holds it while it is out of view or its page is hidden. That makes it easier to read, but it is not a way to stop it.

### What the component handles

- It moves only when the text does not fit. A line whose text fits draws plain text and starts nothing.
- Where reduced motion is requested (WCAG 2.3.3), it never moves. A hover or a focus shows the hidden part a viewport at a time, without a motion, resting on each view, so that every part of the text can still be seen; a line with `trigger="always"` rests cut and draws no playback control.
- Assistive technology reads the text once, whole, as text, where the line stands. The copy, the ellipsis and the fade are hidden from it, and the motion changes nothing it reads and announces nothing.
- The line is not a focus stop. Tab reaches the control it stands in, and the playback control while it is drawn.
- The playback control is a Button of the text variant and the small size, with the icon set’s pause glyph while the text moves and the play glyph while it is paused. Enter or Space toggles it, focus stays on it, and it emits `pausedChange`. Its target meets the minimum.
- In a right-to-left layout the playback control stands at the left end of the line; the text moves by its own direction.
- With forced colors, the text takes the color the platform gives the element around it, and its edges are clipped instead of faded.
- It has no words of its own. A playback control that has no name from the instance or the product’s words is still drawn, and reported in debug builds; so are an unknown `trigger` or `loop` and a speed, a pause or a count that is out of range.

The web component is not the HTML `<marquee>` element. That element is [deprecated](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/marquee), ignores reduced motion and cannot be paused by the person.

### What you need to do

- Meet WCAG 2.2.2 as above: a way to pause every line with `trigger="always"`, and a count of `iterations` that ends a hover’s or a focus’s motion within five seconds.
- Give the playback control its two names, in the instance or once in the product’s words, in the language of the page: “Pause scrolling text” and “Play scrolling text” say what the button does.
- Keep text a person must read in full out of a marquee: a cut line hides it from anyone who never triggers the motion.
- Keep one line that moves all the time to a screen, and none beside a long text a person is reading.
