references/layout.md
# Layout animation: `<AnimeLayoutGroup>` and `useAnimeLayout`
For elements that stay mounted but change position or size: a class toggle,
a grid column count, a card expanding. For `v-for` items entering, leaving or
reordering, use `<AnimeTransitionGroup>` first ([transitions.md](transitions.md)).
- `<AnimeLayoutGroup>` when the change comes from state you can list in `deps`.
- `useAnimeLayout` when you need to `await` the animation or change timing per call.
Both groups take `deps` and `shallow`, but they mean different things:
| | `<AnimeTransitionGroup>` | `<AnimeLayoutGroup>` |
|---|---|---|
| Built for | `v-for` items entering, leaving, reordering | mounted elements changing size or position |
| Enter and leave | `enterAnimation` / `leaveAnimation` per item | `v-show` fades, `v-if` removes at once |
| `deps` | extra triggers on top of list changes (`[]` = list changes only) | the only trigger |
| Timing | `moveAnimation`: `duration`, `delay`, `ease` | `layoutOptions`: any `createLayout()` param |
| What moves | every element inside | every element inside, or `elements` |
Layout options (`children`, `enterFrom`, `leaveTo`, `swapAt`, …):
https://animejs.com/documentation/layout
## `<AnimeLayoutGroup>`
Records its children before Vue updates the DOM whenever `deps` changes,
then animates them to their new size and position.
```vue
<template>
<AnimeLayoutGroup :deps="[view]" :layout-options="{ duration: 500 }" :class="view === 'grid' ? 'grid grid-cols-2' : 'flex'">
<div v-for="item in items" :key="item.id" />
</AnimeLayoutGroup>
</template>
```
- `deps` entries are watched deeply. A getter entry such as
`[() => store.view]` is called and its result watched. Add `shallow` to
react only when an entry is replaced.
- Always pass `deps`. Without it, every re-render of the group measures
every element inside it and animates, even when nothing moved, such as a
parent passing a new object or array prop on every render.
- Each animation measures every element inside the group, whatever
`elements` says. Keep the markup inside lean.
- Toggle children with `v-show` to get enter and leave fades while the rest
slides. With `v-if`, a removed child vanishes at once and only its
neighbours animate.
- `elements` picks which elements move on their own: a selector such as
`'.card'` (matched only inside this group), elements, component instances,
or a list. The group still measures everything inside it, so wrap only
what moves.
- Content inside a matched element does not move on its own: it jumps to its
new spot halfway through, so it can visibly shift when the element resizes.
Add it to `elements` (`'.card, .card h3'`) to make it slide. If its own size
changes too it fades out and back; `layoutOptions: { swapAt: { opacity: 1 } }`
keeps it visible.
- Instead of listing a selector for every element, give each element that
moves the same attribute of your own and pass it once:
`elements="[data-layout]"` with `data-layout` on the card and its `h3`.
- `layoutOptions` takes Anime.js `createLayout()` params. By default the
group moves on `spring({ bounce: 0.15, duration: 300 })`. Setting `ease` or
`duration` anywhere (prop or component defaults) replaces the spring, since
a spring picks its own duration. `tag` defaults to `'div'`. App-wide
defaults: see Component defaults in [transitions.md](transitions.md).
- Layout animates `width` and `height`, so text reflows as a box resizes.
Text that fits on one line when the move ends stays on one line throughout,
so a button whose label changes doesn't wrap mid-move. Text over several
lines reflows.
Docs: https://nanimejs.netlify.app/components/layout-group
## `useAnimeLayout`
```ts
const list = useTemplateRef('list')
const layout = useAnimeLayout(list, { duration: 500, ease: 'inOutQuad' })
async function reverse() {
await layout.patch(() => items.value.reverse())
}
async function toggleWide() {
await layout.patch(() => {
wide.value = !wide.value
}, { duration: 300 })
}
```
- Pass the container, not the items. Its children are recorded and animated.
- Put every state change that moves elements inside the `patch` callback.
`patch` snapshots positions, runs the callback (sync or async), waits for
Vue to update the DOM, then animates. It returns the Anime.js timeline, so
`await` resolves when the animation finishes.
- The optional second argument overrides the timing for that call.
- Call `patch` from handlers or watchers, after mount.
- Don't use Anime.js `layout.update(callback)` for Vue state. It animates
before Vue has patched the DOM, so nothing moves.
- The options are reactive. A change in value rebuilds the layout, so keep
them stable while an animation runs.
- When the change can't go in one callback: `layout.record()`, change state,
`await nextTick()`, `layout.animate()`, in that order.
Docs: https://nanimejs.netlify.app/composables/use-anime-layout
references/lists.md
# Animating lists (`v-for`)
## Add, remove, reorder
Wrap the `v-for` in `<AnimeTransitionGroup>`. New items run the enter
animation, removed items the leave animation, and the rest slide to their
new positions. See [transitions.md](transitions.md), including staggering.
## Draggable items
Give each item its own component, with `useDraggable` inside it. To drop
an item out of the list, hide its handle with `v-if` inside an
`<AnimeTransition>` and remove it from the array only in `@after-leave`.
`useDraggable` reverts its draggable when the component unmounts, and the
revert moves the element back to where the drag started. Removing the item
on release makes it jump back to its slot before it fades.
## Entrance stagger without a transition group
Target the items through the container ref, not a `v-for` ref array.
```ts
import { stagger } from '#nanime/utils'
const grid = useTemplateRef('grid')
useAnimate(
() => grid.value ? [...grid.value.querySelectorAll<HTMLElement>('.card')] : [],
{ opacity: [0, 1], translateY: [40, 0], delay: stagger(80, { grid: [4, 3], from: 'center' }) },
)
```
A getter over a `v-for` ref array changes identity whenever the list grows.
That rebuilds the animation, so every card replays. The getter above tracks
only `grid`, which settles once at mount.
references/timelines.md
# Timelines, split text, SVG and scroll
## Timeline methods can be called in `setup`
`useAnimeTimeline()` returns a buffered proxy. `.add()`, `.set()`, `.label()`,
`.play()` and the rest queue until the timeline exists after mount, then
replay. No `onMounted` or `watch` needed.
```ts
const { chars } = useSplitText(heading, { chars: true })
const tl = useAnimeTimeline({ autoplay: false })
tl.add(chars, { opacity: [0, 1] }, 0) // chars is still [] here, which is fine
```
## Scroll-driven timeline
```ts
const scroll = useAnimeScroll(() => ({ target: section.value ?? undefined, sync: true }))
const tl = useAnimeTimeline({ autoplay: scroll })
```
Bind through `autoplay`, which nanime unwraps to the observer. Don't call
`scroll.link(tl)`: it hands Anime.js the proxy itself.
## SVG helpers need a real element
Composable targets accept refs. `createDrawable`, `morphTo` and
`createMotionPath` from `#nanime/proxies/svg` do not: they read the element
right away, and in top-level `setup` the ref is still `null`.
`createDrawable` returns an array. Take `[0]`: a ref holding an array is not
an array of refs, and targets don't accept it.
Outside a timeline, wrap it in a `computed`:
```ts
const drawable = computed(() => path.value ? createDrawable(path.value)[0] : null)
useAnimate(drawable, { draw: ['0 0', '0 1'], duration: 1000 })
```
In a timeline, add inside `onMounted`:
```ts
onMounted(() => {
if (!path.value || !shape.value) return
const [drawable] = createDrawable(path.value)
if (!drawable) return
tl.add(drawable, { draw: ['0 0', '0 1'], duration: 1000 }, 0)
.add(path.value, { d: morphTo(shape.value), duration: 800 }, 1000)
})
```
## Timing that looks right but traces wrong
These come from Anime.js, not nanime:
- A staggered `delay` with `from` values leaves later items at rest until
each delay starts. Put the start state at position 0 with
`tl.set(targets, {...}, 0)`.
https://animejs.com/documentation/timeline/timeline-methods/set
- A staggered `delay` with `loop` and `alternate` inside a timeline mistimes
the iterations. Stagger the position instead:
`tl.add(targets, params, stagger(30, { start: 1200 }))`.
https://animejs.com/documentation/utilities/stagger/timeline-positions-staggering
- `loop: n` means n *extra* passes. For "go and come back" with `alternate`,
use `loop: 1`.
https://animejs.com/documentation/animation/animation-playback-settings/loop
references/transitions.md
# `<AnimeTransition>` and `<AnimeTransitionGroup>`
Drop-ins for Vue's `<Transition>` and `<TransitionGroup>`. Anime.js runs the
enter, leave and move animations, so write no transition CSS and no
`.v-enter-active` / `.v-move` classes. Vue's transition events
(`@after-enter`, `@after-leave`, …) pass through.
For elements that stay mounted but change position or size, use
[layout.md](layout.md) instead.
Docs: https://nanimejs.netlify.app/components/transitions and
https://nanimejs.netlify.app/components/transition-styles
## One element: `v-if`, `v-show` or a changed `key`
```vue
<template>
<AnimeTransition enter-animation="slide-up" leave-animation="fade">
<div v-if="open" class="panel" />
</AnimeTransition>
<AnimeTransition mode="out-in" enter-animation="swap" leave-animation="swap">
<span :key="status">{{ status }}</span>
</AnimeTransition>
</template>
```
| Prop | Type | Default |
|---|---|---|
| `enterAnimation` | style name or `AnimationParams` | `'fade'` |
| `leaveAnimation` | style name or `AnimationParams` | `'fade'` |
| `mode` | `'default'`, `'in-out'`, `'out-in'` | `'default'` |
| `appear` | `boolean` | `false` |
Use `mode="out-in"` for keyed swaps. Otherwise both elements are in the DOM
at once and the new one pushes the old one aside.
For a crossfade, keep the default mode and stack the two elements in one
grid cell: make the wrapper a `grid` and give the keyed child
`col-start-1 row-start-1`. The old one's leave and the new one's enter then
run together in the same spot. No fixed size or `absolute` is needed: the
wrapper takes the taller of the two while both are mounted, and the new
one's size once the leave ends.
```vue
<template>
<div class="grid">
<AnimeTransition :enter-animation="{ opacity: [0, 1], duration: 400 }" :leave-animation="{ opacity: 0, duration: 400 }">
<img :key="track.id" :src="track.cover" class="col-start-1 row-start-1">
</AnimeTransition>
</div>
</template>
```
## Lists: `v-for` with add, remove and reorder
```vue
<template>
<AnimeTransitionGroup tag="ul" class="relative flex flex-wrap gap-2" :deps="[]" enter-animation="scale" leave-animation="scale">
<li v-for="item in items" :key="item.id">{{ item.label }}</li>
</AnimeTransitionGroup>
</template>
```
It takes the props above except `mode`, with its own defaults, plus:
| Prop | Type | Default |
|---|---|---|
| `enterAnimation` | style name or `AnimationParams` | fade in, 250ms `out(3)` |
| `leaveAnimation` | style name or `AnimationParams` | fade out, 150ms `in(3)` |
| `tag` | `string` | `'div'` |
| `moveAnimation` | style name, `{ duration, delay, ease }`, or `false` | `{ duration: 350, ease: 'out(3)' }` |
| `absoluteLeave` | `boolean` | `true` |
| `deps` | `unknown[]` (values or getters) | |
| `shallow` | `boolean` | `false` |
- Unlike Vue's `<TransitionGroup>`, it always renders a wrapper element
(`tag`). Put the list's layout classes on the group itself.
- Give the group `position: relative`. With `absoluteLeave`, a leaving item
switches to `position: absolute` against it, so the others close the gap
while it animates out.
- Moves (reorder, shuffle, the gap closing) use AnimeJS layout. Mutate the
array and the items slide. No `useAnimeLayout` or `patch()` call is needed.
- Every item needs a stable `:key`, such as an id. With an index key,
removing an item makes the last element leave while the rest swap their
content in place, so nothing slides.
- A leave, a move and an enter in the same update all start together. To
let the new item appear after the others have made room, give the enter
a `delay` about as long as the leave. The enter's start values apply at
once, so the item stays hidden while it waits.
### Pass `deps`
Without `deps`, every re-render of the group measures everything inside
it, even when nothing moved. Pass it on every group:
```vue
<AnimeTransitionGroup :deps="[]"> <!-- add, remove, reorder -->
<AnimeTransitionGroup :deps="[view]"> <!-- also when view resizes items -->
```
- Entries are watched deeply. Add `shallow` to react only when an entry is
replaced.
- Each move measures every element inside the group, down to the text in
each item. Keep item markup lean.
- Use `:move-animation="false"` when items only enter and leave. The group
then measures nothing.
- Without `deps`, a value read in the slot that settles after mount, such
as a template ref passed to each item as a prop, re-renders the group and
runs a move pass. Pass a getter instead (`:target="() => bin"`).
### Staggering items
`stagger()` works in `enterAnimation` and `leaveAnimation`. Items that
enter, or leave, in the same update are counted together in DOM order, so
the first one starts first:
```vue
<script setup lang="ts">
import { stagger } from '#nanime/utils'
</script>
<template>
<AnimeTransitionGroup appear :enter-animation="{ opacity: [0, 1], y: [16, 0], delay: stagger(60) }">
<div v-for="item in items" :key="item.id" />
</AnimeTransitionGroup>
</template>
```
The count restarts with each update, so one item added later starts with
no delay.
A stagger in `moveAnimation` works differently. Moves run on every element
inside the group, down to the text in each item, so the delay grows with
each nested element rather than each item. An item holding five elements
adds five steps, and the text trails behind its own item. Leave the move
unstaggered for items with content, or keep the step to a few
milliseconds.
## Styles
Built-ins: `fade`, `slide-up`, `slide-down`, `slide-left`, `slide-right`,
`scale`, `swap`. Inline params beat a name. An unknown name logs a warning
and falls back to the component's own default: `fade` on `<AnimeTransition>`,
the group's defaults on `<AnimeTransitionGroup>`. The same goes for a style
that lacks the part asked for: no built-in style has a `move`, so
`move-animation="fade"` moves with the group's 350ms `out(3)`.
Define your own in `app.config.ts`, never `nuxt.config.ts`. A style can set
`enter`, `leave` and `move`, and a style named like a built-in replaces it.
```ts
export default defineAppConfig({
nanime: {
transitions: {
pop: {
enter: { opacity: [0, 1], scale: [0.4, 1], duration: 500, ease: 'outBack(3)' },
leave: { opacity: 0, scale: 0.4, duration: 250 },
move: { duration: 300, ease: 'out(3)' },
},
},
},
})
```
Inline params take any Anime.js param, including `keyframes`:
```vue
<AnimeTransition :leave-animation="{ keyframes: [{ x: -8, duration: 80 }, { x: 8, duration: 80 }, { opacity: 0, scale: 1.6, duration: 300 }] }">
<div v-if="visible" />
</AnimeTransition>
```
## Component defaults
Default props for `<AnimeTransition>`, `<AnimeTransitionGroup>` and
`<AnimeLayoutGroup>` go in `app.config.ts` under `nanime.components`, keyed
`transition`, `transitionGroup` and `layoutGroup`:
```ts
export default defineAppConfig({
nanime: {
components: {
transition: { enterAnimation: 'slide-up' },
transitionGroup: { moveAnimation: { duration: 300 } },
},
},
})
```
`provideAnimeDefaults({ ... })` takes the same shape and applies to the
calling component's subtree. A prop passed to the component always wins, and
an `undefined` default leaves the outer one in place. In app config,
`layoutGroup.elements` takes selectors only.
references/verifying.md
# Verifying an animation without a browser
Don't watch the page. Mount the component, seek its instance, and read the
styles.
## Setup
The Nuxt app under test must register the module:
```ts
// nuxt.config.ts
export default defineNuxtConfig({ modules: ['nanime'] })
```
If you get `Missing "#nanime/..." specifier` or the composables are undefined,
the module isn't registered in the app that vitest runs. The problem is not the
import path, so don't switch to raw `animejs` imports.
## Trace
```ts
// component: defineExpose({ tl })
const wrapper = await mountSuspended(Hero)
await new Promise(r => setTimeout(r, 0)) // let mount-time builds flush
const { tl } = wrapper.vm
tl.pause()
const at = (t: number) => (tl.seek(t), el.style.transform)
expect(at(0)).toContain('translateX(-100%)')
expect(at(400)).toContain('translateX(0%)')
```
- Assert the start, a midpoint, the end of each segment, and `tl.duration`.
- Anime.js writes x/y together as `translate(x, y)`, so parse the numbers
instead of matching `translateX(`.
- For data-driven updates, change the reactive value mid-flight and check that
the value continues from where it was without snapping to the start. That
only works with `keepTime` (SKILL.md, "Rebuilds restart the animation").
## happy-dom gaps
These gaps are in the test environment. Your code isn't at fault.
| Symptom | Polyfill |
|---|---|
| `useSplitText`: `Cannot read properties of undefined (reading 'status')` | `document.fonts = { status: 'loaded', ready: Promise.resolve() }` |
| `useAnimeLayout().patch()` or `<AnimeTransitionGroup>` moves: `$el.animate is not a function`, reported as an unhandled rejection | a stub `Element.prototype.animate` that returns an object with `play/pause/cancel/finish`, `playbackRate`, `onfinish/oncancel/onremove` and a `finished` promise |
happy-dom does no layout, so every rect is 0. Drag distance, scroll progress and
layout slide distance can't be measured here. For those, assert that the
instance ran and where it ended up.
SKILL.md
---
name: nanime
description: |
Build animations in a Nuxt app with nanime, the Nuxt module wrapping Anime.js v4.
Use when animating elements, text, SVG or scroll in Nuxt: useAnimate, useWaapiAnimate,
useAnimatable, useAnimeTimeline, useDraggable, useSplitText, useScrambleText,
useAnimeScroll, useAnimeLayout, <AnimeTransition>, <AnimeTransitionGroup>,
<AnimeLayoutGroup>, transition styles, component defaults, provideAnimeDefaults and
the #nanime/* aliases. Also use when an effect seems to need raw animejs, GSAP or CSS
transition classes. Most mistakes here do not throw; they animate the wrong thing.
---
# nanime
The composables own an Anime.js instance, rebuild it when reactive inputs
change, and revert it on unmount. They wait for mount, so they are SSR-safe.
Composables and components are auto-imported and auto-registered.
Don't drop to `onMounted` with raw `animejs` imports, and don't reach for GSAP
or transition CSS. The effect almost always has a nanime path.
## Pick the tool
| Building | Use | Details |
|---|---|---|
| Animating elements, refs, arrays, plain objects | `useAnimate` (`useWaapiAnimate` for the Web Animations API) | |
| Values set imperatively at speed (cursor follow, live counter) | `useAnimatable` | |
| Sequencing | `useAnimeTimeline` | [timelines.md](references/timelines.md) |
| Scroll-driven animation | `useAnimeScroll` | [timelines.md](references/timelines.md) |
| Text split into lines, words, chars | `useSplitText` | [timelines.md](references/timelines.md) |
| Scrambling or revealing text | `useScrambleText` | |
| SVG draw, morph, motion path | `#nanime/proxies/svg` inside a composable | [timelines.md](references/timelines.md) |
| Dragging with snap, bounds, axis locks | `useDraggable` | [lists.md](references/lists.md) for draggable list items |
| `v-if` / `v-show` / keyed enter and leave, crossfades | `<AnimeTransition>` | [transitions.md](references/transitions.md) |
| `v-for` add, remove, reorder, stagger | `<AnimeTransitionGroup>` | [transitions.md](references/transitions.md), [lists.md](references/lists.md) |
| Mounted elements changing position or size (FLIP) | `<AnimeLayoutGroup>`, or `useAnimeLayout` to `await` it | [layout.md](references/layout.md) |
| Testing an animation without a browser | | [verifying.md](references/verifying.md) |
For an Anime.js parameter, return shape or util, read
https://animejs.com/documentation. nanime passes those through unchanged.
For nanime's own API, read https://nanimejs.netlify.app/llms-full.txt or query
the docs MCP server at https://nanimejs.netlify.app/mcp.
## Aliases
Not auto-imported. Import helpers from these, never from `animejs` directly.
| Alias | Holds |
|---|---|
| `#nanime/types` | Anime.js types (`AnimationParams`, `JSAnimation`, …) and `AnimeTransitionStyle` |
| `#nanime/utils` | Anime.js utils (`stagger`, `random`, `set`, …), plus `animate` and `createTimer` for handler code |
| `#nanime/easings` | `spring`, `cubicBezier`, `steps`, … |
| `#nanime/proxies/svg` | `createMotionPath`, `createDrawable`, `morphTo` |
| `#nanime/proxies/text` | `scrambleText`, for use inside animation parameters |
| `#nanime/proxies` | Both of the above |
## Rules
Each of these fails without an error.
### Parameters that read a template ref must be a getter
```ts
useAnimeScroll(() => ({ target: section.value ?? undefined })) // correct
useAnimeScroll({ target: section.value }) // silently wrong
```
A template ref is `null` during setup. A plain object is read once, then, so
Anime.js falls back to a default, such as watching `document.body` instead of
your element. A getter is read again once the element exists.
Read `.value` yourself: Anime.js does not unwrap Vue refs inside parameters.
Template refs are `T | null` while most params take `T | undefined`, so use
`?? undefined`, not a cast.
### Call composables only in `setup`
```ts
function onClick() {
useAnimate(box, { x: 100 }) // leaks: no effect scope, never reverted
}
```
From a handler or per-frame callback, use `animate` from `#nanime/utils` and
own the handle:
```ts
import { animate } from '#nanime/utils'
import type { JSAnimation } from '#nanime/types'
const pulse = shallowRef<JSAnimation | null>(null)
onScopeDispose(() => pulse.value?.revert())
function highlight(el: HTMLElement) {
pulse.value?.revert()
pulse.value = animate(el, { scale: [1, 1.1, 1], duration: 400 })
}
```
Code of your own that reads an element must also wait for mount.
### Pass composable returns straight in
nanime unwraps a composable's return value to its Anime.js instance, in
parameters and in a timeline's `sync()`:
```ts
const scroll = useAnimeScroll(() => ({ target: section.value ?? undefined, sync: true }))
useAnimate(box, { x: 400, autoplay: scroll })
```
One scroll observer drives one animation. Handing it to a second animation
steals it from the first, which freezes. Call `useAnimeScroll` once per
animation; observers with the same params stay in step.
### Live values do not tick in a template
`{{ animation.progress }}` never updates; the return value tracks the
instance, not the numbers inside it. Copy the value out from a callback:
```ts
const progress = ref(0)
useAnimate(box, { x: 400, onUpdate: self => progress.value = self.progress })
```
### Text a composable rewrites must not re-render
`useScrambleText` and `useSplitText` write into the element. If Vue also
renders the changing value there, they overwrite each other. Render the first
value once and hand changes to the composable:
```vue
<script setup lang="ts">
useScrambleText(title, {}, () => ({ text: track.value.title }))
</script>
<template>
<h2 ref="title" v-once>{{ track.title }}</h2>
</template>
```
For `useSplitText`, keep `v-once` and pass the new text through its `html`
option.
### Scramble timing goes in the third argument
AnimeJS `scrambleText()` sets its own `duration`, `delay` and `ease`, so the
same keys in `useScrambleText`'s second argument are ignored. Keep the second
argument for playback and callbacks:
```ts
useScrambleText(title, { onComplete: next }, () => ({ text: text.value, duration: 2000 }))
```
### Give list and layout groups `deps`
Without `deps`, `<AnimeTransitionGroup>` and `<AnimeLayoutGroup>` measure
every element inside them on each re-render, even when nothing moved. A
parent that passes a new object or array prop on every render re-renders
them each time. Pass `deps` by default:
```vue
<AnimeTransitionGroup :deps="[]"> <!-- moves on add, remove, reorder -->
<AnimeTransitionGroup :deps="[view]"> <!-- also when view resizes items -->
<AnimeLayoutGroup :deps="[expanded]"> <!-- only when expanded changes -->
```
List every value whose change moves or resizes items. `:deps="[]"` on an
`<AnimeLayoutGroup>` turns its animation off. Each move still measures every
element inside the group, so keep item markup lean. On an
`<AnimeTransitionGroup>` whose items only enter and leave, set
`:move-animation="false"`.
### Reduced motion is a module option
`nanime.reducedMotion` in `nuxt.config.ts` takes `'never'` (default),
`'system'` (follows `prefers-reduced-motion`) or `'always'`. When motion is
reduced, animations from the composables, transitions and groups jump to
their end state. `useDraggable` and `useAnimeScroll` are not affected. To
let visitors choose, call `updateAppConfig({ nanime: { reducedMotion } })`.
Don't add your own `prefers-reduced-motion` checks around nanime calls.
### Rebuilds restart the animation
When a reactive target or parameter changes, the composable reverts the old
instance and builds a new one, from the start. Unchanged params are skipped,
so a getter returning an identical fresh object costs nothing.
- To continue mid-flight instead, pass `{ keepTime: true }` as the last
argument. Only `useAnimate`, `useAnimeTimeline` and `useScrambleText` take it.
- For a value that retargets constantly, use `useAnimatable`.