remix

Build and review Remix 3 applications using the `remix` npm package and subpath imports. Use when working on Remix app structure, routes, controllers, middleware, validation, data access, auth, sessions, file uploads, server setup, UI components, hydration, navigation, or tests.

Install
npx skills add 'https://github.com/kentcdodds/kody/tree/main/.agents/skills/remix'
Download bundle ↓
main · 8beb6cdScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

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

PresetBounceDurationCharacter
smooth-0.3400msOverdamped, no overshoot
snappy0200msCritically damped, quick
bouncy0.3400msUnderdamped, 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.
Referenced from SKILL.md