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
background-size: calc(p × 100%) 100%progress pthe text, filled up to p × its length0.00Each word lights up as you read it0.25Each word lights up as you read it0.50Each word lights up as you read it0.75Each word lights up as you read it1.00Each word lights up as you read it← fill front, at p × the length00.51share of the text filled, in reading order
Fig. 1 — the fill front at five values of progress. Scrolling sets p, 0 at the start line and 1 at the end line; the gradient behind the text is filled up to p × its length.
01The tool

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.

Engine

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

Progress0.00

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
Text
Element
Fill · gradient
Direction

Line by line: one strip runs through the text in reading order, so the next line starts where the last one ended.

Colours
Scroll range
Start · unfilled · 'top 80%'
End · filled · 'bottom center'
After it fills

A CSS scroll timeline always follows the scroll: scrolling back up unfills the text. To keep it filled, use scrollProgress or scrollText.

text-fill.css32 lines
/* 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.

Run it
02The mechanism

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).

the idea.css12 lines
.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).

<p><span>…</span></p> · reading orderWords light up oneafter anotherline 1line 20p = 0.71display: inline70% of the strip:all of line 1, then37 of line 2’s 130<p>…</p> · left to rightWords light up oneafter anotherp = 0.7display: block70% of its width,on every line at once
Fig. 2 — the same progress, 0.7, on the two boxes. An inline span’s background runs through its lines as one strip; a block’s is one rectangle.
Where p comes from
The CSS engine animates 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%).
The range
The generator writes each trigger point as an 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.
033 engines

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.

AspectCSS onlyscrollProgressscrollText
What changesA colour gradient behind the letters.The same gradient.Each piece’s opacity, from from.opacity to 1.
The fillA 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 stateAny colour.Any colour.The text colour at a lower opacity, so a blend of it and the background.
JavaScriptNone.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.
BrowsersThose with animation-timeline. The others show the text filled, from the rule outside @supports.Wherever the script runs.Wherever the script runs.
Stay filledNo: a scroll timeline always follows the scroll.With onProgress keeping the furthest value, as the generator writes it.once: true, its default.
Reduced motionStill fills: nothing moves.Still fills.Shows the text in full at once.
Scrolls withThe nearest scrolling box: the window, or a panel.The window, or the box given as scrollContainer.The window only.
In a panel
scrollText follows the window’s scroll and has no option for another scroller. For text inside a scrolling panel, use the CSS engine, whose view timeline finds the nearest scrolling box, or scrollProgress with scrollContainer, as the preview above does.
Plain text
scrollText splits the element’s text content, so markup inside it, a link or an <em>, comes out as plain text. The gradient engines leave the markup alone.
04Accessibility

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.

reduced-motion.css8 lines
/* 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%;
  }
}
05Next

Use it in your project

Colour fill
The gradient engine’s script is scrollProgress, from svg-scroll-draw/progress: scrollProgress in the docs. The CSS-only engine needs no package at all.
067 questions

Text fill on scroll questions

Q1

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.

Q2

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.

Q3

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.

Q4

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.

Q5

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.

Q6

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.

Q7

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.