-
Notifications
You must be signed in to change notification settings - Fork 1
Recipes
Each recipe is complete and runs as written, with nothing to fill in. For the reference, see API. For why the motion behaves as it does, see How the roll works.
The common case. It also shows why update() and the value setter are separate calls.
<scritto-text id="count">0</scritto-text>
<script type="module">
import '@scritto/core'
const el = document.querySelector('#count')
let n = 0
setInterval(() => el.update(String(++n)), 1000)
</script>update() rolls. Assigning el.value sets the text with no motion, which is what you want for the first value and not for the ones after it.
Intl.NumberFormat produces the grouping separators and the currency symbol, and Scritto keeps whichever glyphs survive. $1,204.00 → $1,205.00 rolls one digit and holds the rest still.
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
const setPrice = (cents) => el.update(money.format(cents / 100))
setPrice(120400)
setPrice(120500) // only the 4 rollsFeed it the formatted string, never the raw number. The formatter is what puts the separator in, and the separator is what keeps the digits in stable columns.
Wrap the line so the words after the value move with it instead of jumping when the browser reflows.
<scritto-flow>
You have <scritto-text id="unread">1</scritto-text> unread message.
</scritto-flow>Give the flow at least 16px of horizontal room if its container hides overflow. The edge fade needs one rem to finish, and a container with 8px of padding and overflow: hidden cuts it off partway. See Flow.
There is no "done" event. Read it off the animations, and take both sets: the glyphs live in the shadow tree and the box does not.
const settled = (el) =>
new Promise((resolve) => {
el.addEventListener('scrittochange', function done(event) {
if (event.detail.phase !== 'after') return
el.removeEventListener('scrittochange', done)
const anims = [...el.shadowRoot.getAnimations(), ...el.getAnimations()]
Promise.all(anims.map((a) => a.finished.catch(() => {}))).then(resolve)
})
})
const finished = settled(el) // attach BEFORE the update
el.update('1,000,000')
await finished // measured: 702ms for a 550ms rollAttach the listener before calling update(). update() commits in a microtask, so the animations do not exist yet on the line after the call. Reading them immediately returns an empty list and a promise that resolves in about 1ms. Wait for the after phase, which is when the glyphs exist.
This never resolves if the value did not change, because an unchanged value fires no event at all. Compare against el.value first if that is possible in your code.
An element that rolls on first paint animates from empty, which reads as the page loading twice.
el.value = '1,204' // no motion
el.update('1,205') // rolls from here onServer-rendering does the same job: text inside the tag is the starting value. Import @scritto/core/ssr.css if it has to look right before the script runs.
import Scritto from '@scritto/react'
function Cart({ total }) {
return <Scritto value={total} trend={1} />
}The wrapper does not animate on mount, and animates on every change after it. trend={1} makes the glyphs arrive from below. Leave it off and the direction is read from the numbers.
trend: 0, the default, compares each number in the old value with its counterpart in the new one and lets the first differing pair decide. That is right for a counter and wrong for something like a shuffled label, where consecutive values have no order.
el.setOptions({ trend: 1 }) // always arrive from belowValues that gain or lose a number entirely read as a rise, because there is nothing to compare position for position.
el.setOptions({ transition: { duration: 900 }, bounce: true })duration is one glyph's animation, not the whole change: the last glyph starts late, so a 400ms duration finishes somewhere past 460ms. Timing has the arithmetic.
Keep the duration shorter than the gap between your updates. A value that changes faster than its own roll stacks ghosts, the copies of glyphs that are leaving, on screen. One is legible; four are not.
Nothing to do. respectMotionPreference is on by default, so a reader with prefers-reduced-motion: reduce gets the new value with no animation. An element scrolled off screen also renders instantly rather than animating out of view.
el.setOptions({ respectMotionPreference: false }) // opt out of the preference