tool / text fillcss · javascript · react
Text fill on scroll generator
A heading or a paragraph that fills with colour as it scrolls into view, or lights up word by word: the highlight from product pages. Make it in plain CSS on a scroll timeline, or with svg-scroll-draw, scroll the preview, and copy the CSS, HTML, JavaScript or React.
- Engines
- 3
- Directions
- 3
- Splits
- 3
- Library
- 2.23.3
Fill text as it scrolls
The box below is a small viewport. Scroll it, drag the slider or press Play: the text fills between the start and end lines, and what fills it is the engine you picked, given your settings and the box in place of the window.
A colour gradient clipped to the text, sized by a CSS scroll timeline (animation-timeline: view()). No JavaScript; browsers without scroll timelines show the text filled.
Viewport · 320px tall · scroll it
Words light up one after another as you read them, until the whole sentence is lit.
start edge · top end edge · bottom
Before the start line: unfilled.
- Starts
- when its top reaches 80% down the viewport
- Done
- when its bottom reaches the middle of the viewport
- CSS
animation-range: entry-crossing 20vh entry-crossing calc(100% + 50vh)- Contrast
- unfilled 3.23:1 · filled 17.27:1unfilled text meets 3:1, enough for large text only
/* Made with https://svg-scroll-draw.vercel.app/text-fill-scroll-generator */
.text-fill {
color: #f2f0eb;
}
/* With scroll timelines it fills as it scrolls; without, it stays as above. */
@supports (animation-timeline: view()) {
.text-fill {
view-timeline-name: --text-fill;
}
.text-fill > span {
color: transparent;
background: linear-gradient(#f2f0eb, #f2f0eb) 0 0 / 0% 100% no-repeat #64625d;
-webkit-background-clip: text;
background-clip: text;
animation-name: text-fill;
animation-duration: auto;
animation-timing-function: linear;
animation-fill-mode: both;
animation-timeline: --text-fill;
animation-range: entry-crossing 20vh entry-crossing calc(100% + 50vh);
}
/* Selected text stays readable: the background’s colour on the fill colour. */
.text-fill > span::selection {
color: #0b0b0c;
background: #f2f0eb;
}
}
@keyframes text-fill {
to { background-size: 100% 100%; }
}No JavaScript. The range is written in vh: on the page, the window is the viewport.
How the fill works
The letters are see-through, and what shows through them is the background, clipped to their shapes by background-clip: text. The background has two colours: the unfilled one everywhere, and over it a gradient layer of the filled colour, sized to the progress. At progress 0 that layer has no width; at 1 it covers the text (fig. 1).
.text-fill > span {
/* The letters show only what is behind them. */
color: transparent;
background:
/* The filled colour, p of the way along... */
linear-gradient(#f2f0eb, #f2f0eb) 0 0 / calc(var(--p) * 100%) 100% no-repeat
/* ...and the unfilled colour under it. */
#64625d;
/* Both painted inside the letters only. */
-webkit-background-clip: text;
background-clip: text;
}Which box carries the background decides the direction. Put it on an inline <span> and the fill runs through the text in reading order, because an inline box’s background is laid out as if its lines were one line, end to end. Put it on the block and one front crosses every line at once; size it downwards instead and it fills from the top (fig. 2).
background-size on a view timeline: the browser’s own scroll progress for the block, between the two points of animation-range. scrollProgress measures the same window in JavaScript and writes it as --scroll-progress on the block, for the CSS to read with calc(var(--scroll-progress, 0) * 100%).entry-crossing position: 0% is the block’s top on the viewport’s bottom and 100% its bottom there, whatever its height, and from there the edge still has (100 − line)% of the viewport to go. So 'bottom 50%' is entry-crossing calc(100% + 50vh), and the two say the same thing to the pixel.CSS, scrollProgress or scrollText
The two gradient engines draw exactly the same thing; they differ in what drives it. scrollText is a different effect, words that brighten one after another, and the one to use when a sentence should light up a word at a time.
| Aspect | CSS only | scrollProgress | scrollText |
|---|---|---|---|
| What changes | A colour gradient behind the letters. | The same gradient. | Each piece’s opacity, from from.opacity to 1. |
| The fill | A smooth front through the text, in any of the three directions. | The same. | A word, letter or line at a time, in reading order, overlapping as the stagger allows. |
| Unfilled state | Any colour. | Any colour. | The text colour at a lower opacity, so a blend of it and the background. |
| JavaScript | None. | scrollProgress writes --scroll-progress and its eased twin on each frame while the text is on screen. | scrollText writes every piece’s opacity on each frame while the text is on screen. |
| Browsers | Those with animation-timeline. The others show the text filled, from the rule outside @supports. | Wherever the script runs. | Wherever the script runs. |
| Stay filled | No: a scroll timeline always follows the scroll. | With onProgress keeping the furthest value, as the generator writes it. | once: true, its default. |
| Reduced motion | Still fills: nothing moves. | Still fills. | Shows the text in full at once. |
| Scrolls with | The nearest scrolling box: the window, or a panel. | The window, or the box given as scrollContainer. | The window only. |
scrollContainer, as the preview above does.<em>, comes out as plain text. The gradient engines leave the markup alone.Readable before, during and after
The text stays text. The gradient engines change nothing but the paint: one text node, read by screen readers, indexed by search engines and selectable like any other. The CSS adds a ::selection rule, because selected see-through letters would otherwise show the browser’s highlight colour where the letters should be.
Split, but read once. scrollText wraps each word, letter or line in a span marked aria-hidden and adds one visually hidden copy of the whole text, which cannot be selected, so a screen reader reads the sentence once and a copy picks up the visible text once. Why a hidden copy and not an aria-label.
The unfilled text is still text. Someone may read a paragraph faster than they scroll it, so the dim state needs contrast too: WCAG asks 4.5:1 for body text and 3:1 for large text. The generator shows the ratio for both states as you pick the colours, the scrollText one blended from the text colour and its opacity as the browser composites it.
Reduced motion. For readers who ask their system for less motion, scrollText shows the text in full at once, and switches to that if they turn the setting on mid-visit. The gradient engines move nothing, only change colour in place, so they keep filling; to show the text filled for those readers as well, add this after the generated CSS.
/* Optional: show it filled for readers who ask for less motion. */
@media (prefers-reduced-motion: reduce) {
.text-fill,
.text-fill > span {
animation: none;
background-size: 100% 100%;
}
}Use it in your project
svg-scroll-draw/progress: scrollProgress in the docs. The CSS-only engine needs no package at all.Text fill on scroll questions
How do I make text fill with colour on scroll?
Paint the text with its background: give it color: transparent and background-clip: text, and put two colours behind it, the filled one as a gradient layer sized to the progress and the unfilled one under it. Then let the scroll set the progress, so background-size runs from 0% to 100% as the text crosses the viewport. In CSS a view timeline does that (animation-timeline); with svg-scroll-draw, scrollProgress writes the progress to --scroll-progress and the CSS sizes the gradient with calc(var(--scroll-progress) * 100%). The generator above writes either.
Can I make a text reveal on scroll with CSS only?
Yes, in browsers with CSS scroll-driven animations. Animate background-size in a @keyframes rule, attach it to a view timeline on the text block with animation-timeline, and say where it starts and ends with animation-range: the generator’s default is entry-crossing 20vh entry-crossing calc(100% + 50vh). Put the transparent text and the gradient inside @supports (animation-timeline: view()), so a browser without scroll timelines shows ordinary, fully coloured text. What CSS cannot do is keep the text filled once it has been read, or split it into words for you.
How do I make the Apple-style text highlight that lights up word by word?
Split the text into words and take each one from dim to full as it scrolls, one after another. scrollText does the splitting: scrollText(el, { from: { opacity: 0.3 }, stagger: 0.05 }) leaves every word where it is at 30% opacity and brings each to full opacity in turn as the text crosses the viewport. For one word strictly after another, set the stagger to 1 ÷ the number of words; the generator has a button for it. For a smooth colour sweep through the words instead, use one of the gradient engines.
Why does my text fill every line at once instead of line by line?
The gradient is on a block. A block’s background is one rectangle, so a fill 50% across it is half of every line. Wrap the text in an inline <span> inside the block and put the gradient on the span: an inline box’s background is laid out as if its lines were set end to end (box-decoration-break: slice, the default), so 50% is the first half of the text in reading order, line after line. Fig. 2 draws both.
Is text that fills on scroll accessible?
It can be. With the gradient the text itself is never changed: it stays one ordinary piece of text that screen readers read, search engines index and readers can select. scrollText wraps each piece in an aria-hidden span and adds one visually hidden copy of the whole text, so a screen reader reads the sentence once, in full. Two things are worth checking: the unfilled text is still text someone may be reading, so give it enough contrast (the generator shows the ratio), and readers who ask for reduced motion get scrollText’s text in full straight away.
How do I keep the text filled after it scrolls past?
Use once. scrollText has it on by default (once: true): it keeps the furthest progress it has reached, so scrolling back up leaves the words lit. scrollProgress has no once of its own, so the generator’s code keeps the furthest value in an onProgress callback and writes it to --text-fill for the CSS to read. A CSS scroll timeline always follows the scroll, both ways.
How do I add text fill on scroll in React or Next.js?
For the word-by-word fill, render <ScrollText> from svg-scroll-draw/react with the text as its child and the options as props, such as from={{ opacity: 0.4 }}; its module is marked use client, so it can be imported from a Server Component. For the colour fill, call scrollProgress in a useEffect and return its destroy(), in a component marked use client, as the React tab writes it. The CSS-only engine needs no JavaScript: a className and the stylesheet.
The rest of svg-scroll-draw 2.23.3: the docs, the scroll trigger visualizer for the start and end lines, and the CSS scroll-driven animation generator.