Files
dotfiles/dot_agents/skills/remix/references/animate-elements.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

4.9 KiB

Animating Elements

What This Covers

How to animate insertion, removal, and layout changes of elements. Read this when the task involves:

  • Adding entrance, exit, or shared-layout transitions to UI
  • Choosing between spring physics (spring(...)) and time-based easing (tween)
  • Coordinating CSS transitions with the same easing as JS animations
  • Imperative animation loops via requestAnimationFrame

Import animation APIs from remix/ui/animation. For the smaller set of animation helpers that show up alongside other mixins, see mixins-styling-events.md.

Animation Mixins

animateEntrance(config)

Animates an element when inserted. Config specifies the starting style the element animates from:

<div
  mix={animateEntrance({
    opacity: 0,
    transform: 'translateY(8px)',
    ...spring('smooth'),
  })}
/>

animateExit(config)

Animates an element when removed. Config specifies the ending style the element animates to. The element stays in the DOM until the animation completes:

{
  isVisible && (
    <div
      key="panel"
      mix={[
        animateEntrance({ opacity: 0, transform: 'scale(0.98)', ...spring('smooth') }),
        animateExit({ opacity: 0, duration: 120, easing: 'ease-in' }),
      ]}
    />
  )
}

animateLayout(config?)

Animates layout changes (position/size) using FLIP-style transforms:

{
  items.map((item) => (
    <li key={item.id} mix={animateLayout({ ...spring({ duration: 500, bounce: 0.2 }) })} />
  ))
}

Options: duration (default 200ms), easing (default spring snappy), size (default true — include scale projection for size changes).

Combining mixins

<div
  key="card"
  mix={[
    animateEntrance({ opacity: 0, transform: 'scale(0.95)', ...spring('snappy') }),
    animateExit({ opacity: 0, transform: 'scale(0.98)', duration: 120, easing: 'ease-in' }),
    animateLayout({ duration: 220, easing: 'ease-out' }),
  ]}
/>

Shared-layout swap

<div mix={css({ display: 'grid', '& > *': { gridArea: '1 / 1' } })}>
  {stateA ? (
    <div key="a" mix={[animateEntrance({ opacity: 0 }), animateExit({ opacity: 0 })]} />
  ) : (
    <div key="b" mix={[animateEntrance({ opacity: 0 }), animateExit({ opacity: 0 })]} />
  )}
</div>

Spring API

Physics-based spring animation. Returns a SpringIterator with duration, easing, and toString() for CSS.

Presets

Preset Bounce Duration Character
smooth -0.3 400ms Overdamped, no overshoot
snappy 0 200ms Critically damped, quick
bouncy 0.3 400ms Underdamped, visible bounce
spring('bouncy')
spring('snappy')
spring('smooth')
spring('bouncy', { duration: 300 }) // override duration

Custom spring

spring({ duration: 500, bounce: 0.3 })
spring({ duration: 500, bounce: 0.3, velocity: 2 }) // continue momentum from gesture

Spread into animation mixins

Spreading a spring gives both duration and easing:

animateEntrance({ opacity: 0, ...spring('bouncy') })

CSS transitions

The iterator stringifies to "550ms linear(...)":

css({ transition: `width ${spring('bouncy')}` })

Or use the spring.transition() helper for multiple properties:

css({ transition: spring.transition('width', 'bouncy') })
css({ transition: spring.transition(['left', 'top'], 'snappy') })

Web Animations API

element.animate(keyframes, { ...spring('bouncy') })

JS iteration

The iterator yields position values from 0 to 1, one per frame:

for (let t of spring('bouncy')) {
  let x = from + (to - from) * t
  updateSomething(x)
  await nextFrame()
}

Tween API

Generator-based tween for animating values over time with cubic bezier easing. Prefer animation mixins or CSS transitions with spring for most UI work. Use tween for imperative requestAnimationFrame loops, canvas/WebGL, or non-CSS properties.

import { tween, easings } from 'remix/ui/animation'

let animation = tween({
  from: 0,
  to: 100,
  duration: 300,
  curve: easings.easeOut,
})

animation.next() // initialize
function tick(timestamp: number) {
  if (handle.signal.aborted) return
  let { value, done } = animation.next(timestamp)
  element.style.transform = `translateX(${value}px)`
  if (!done) requestAnimationFrame(tick)
}
requestAnimationFrame(tick)

Built-in easings: easings.linear, easings.ease, easings.easeIn, easings.easeOut, easings.easeInOut.

Practical Guidance

  • Always key conditional or switching elements you expect to animate.
  • Use animateLayout only on the element whose position or size changes.
  • Prefer one clear transition intent per mixin: entrance starts from a style, exit ends at a style.
  • Default to ...spring() for duration and easing in most cases.
  • Keep DOM work in handle.queueTask(...) or ref(...), not in render.