Easing and transitions
Easing curves, --ease-*, --duration-* and --transition-* variables.
On this page
Systhema ships the easings.net (opens in new tab) curves for motion that feels deliberate: sine, quad, cubic, quart, quint, expo, circ and back, each as in, out and in-out. Every curve exists in three forms generated from one table, so they never disagree: an ease-* utility for transitions, an animate-ease-* utility for animations, and an --ease-* custom property for your own CSS.
Quick referenceLink to this section
| Variable | Value | Class |
|---|---|---|
--ease-in-sine | cubic-bezier(0.12, 0, 0.39, 0) | ease-in-sine |
--ease-out-sine | cubic-bezier(0.61, 1, 0.88, 1) | ease-out-sine |
--ease-in-out-sine | cubic-bezier(0.37, 0, 0.63, 1) | ease-in-out-sine |
--ease-in-quad | cubic-bezier(0.11, 0, 0.5, 0) | ease-in-quad |
--ease-out-quad | cubic-bezier(0.5, 1, 0.89, 1) | ease-out-quad |
--ease-in-out-quad | cubic-bezier(0.45, 0, 0.55, 1) | ease-in-out-quad |
--ease-in-cubic | cubic-bezier(0.32, 0, 0.67, 0) | ease-in-cubic |
--ease-out-cubic | cubic-bezier(0.33, 1, 0.68, 1) | ease-out-cubic |
--ease-in-out-cubic | cubic-bezier(0.65, 0, 0.35, 1) | ease-in-out-cubic |
--ease-in-quart | cubic-bezier(0.5, 0, 0.75, 0) | ease-in-quart |
--ease-out-quart | cubic-bezier(0.25, 1, 0.5, 1) | ease-out-quart |
--ease-in-out-quart | cubic-bezier(0.76, 0, 0.24, 1) | ease-in-out-quart |
--ease-in-quint | cubic-bezier(0.64, 0, 0.78, 0) | ease-in-quint |
--ease-out-quint | cubic-bezier(0.22, 1, 0.36, 1) | ease-out-quint |
--ease-in-out-quint | cubic-bezier(0.83, 0, 0.17, 1) | ease-in-out-quint |
--ease-in-expo | cubic-bezier(0.7, 0, 0.84, 0) | ease-in-expo |
--ease-out-expo | cubic-bezier(0.16, 1, 0.3, 1) | ease-out-expo |
--ease-in-out-expo | cubic-bezier(0.87, 0, 0.13, 1) | ease-in-out-expo |
--ease-in-circ | cubic-bezier(0.55, 0, 1, 0.45) | ease-in-circ |
--ease-out-circ | cubic-bezier(0, 0.55, 0.45, 1) | ease-out-circ |
--ease-in-out-circ | cubic-bezier(0.85, 0, 0.15, 1) | ease-in-out-circ |
--ease-in-back | cubic-bezier(0.36, 0, 0.66, -0.56) | ease-in-back |
--ease-out-back | cubic-bezier(0.34, 1.56, 0.64, 1) | ease-out-back |
--ease-in-out-back | cubic-bezier(0.68, -0.6, 0.32, 1.6) | ease-in-out-back |
Elastic and bounce curves can't be written as a single cubic-bezier(), so they are not part of the set.
Basic usageLink to this section
Each dot below crosses its track with one curve over the same second. Press Play to send them back the other way. in curves start slowly, out curves land softly, in-out do both, and back overshoots.
import { useEffect, useState } from 'react'
import { Button } from '@systhemaui/next'
const curves: [string, number, number, number, number][] = [
['ease-in-sine', 0.12, 0, 0.39, 0],
['ease-out-sine', 0.61, 1, 0.88, 1],
['ease-in-out-sine', 0.37, 0, 0.63, 1],
['ease-in-quad', 0.11, 0, 0.5, 0],
['ease-out-quad', 0.5, 1, 0.89, 1],
['ease-in-out-quad', 0.45, 0, 0.55, 1],
['ease-in-cubic', 0.32, 0, 0.67, 0],
['ease-out-cubic', 0.33, 1, 0.68, 1],
['ease-in-out-cubic', 0.65, 0, 0.35, 1],
['ease-in-quart', 0.5, 0, 0.75, 0],
['ease-out-quart', 0.25, 1, 0.5, 1],
['ease-in-out-quart', 0.76, 0, 0.24, 1],
['ease-in-quint', 0.64, 0, 0.78, 0],
['ease-out-quint', 0.22, 1, 0.36, 1],
['ease-in-out-quint', 0.83, 0, 0.17, 1],
['ease-in-expo', 0.7, 0, 0.84, 0],
['ease-out-expo', 0.16, 1, 0.3, 1],
['ease-in-out-expo', 0.87, 0, 0.13, 1],
['ease-in-circ', 0.55, 0, 1, 0.45],
['ease-out-circ', 0, 0.55, 0.45, 1],
['ease-in-out-circ', 0.85, 0, 0.15, 1],
['ease-in-back', 0.36, 0, 0.66, -0.56],
['ease-out-back', 0.34, 1.56, 0.64, 1],
['ease-in-out-back', 0.68, -0.6, 0.32, 1.6],
]
export default function Demo() {
const [end, setEnd] = useState(false)
useEffect(() => {
const timer = setTimeout(() => setEnd(true), 400)
return () => clearTimeout(timer)
}, [])
return (
<div className="flex w-full flex-col items-center gap-6">
<div className="grid w-full grid-cols-1 gap-x-8 gap-y-4 md:grid-cols-3">
{curves.map(([name, x1, y1, x2, y2]) => (
<div key={name} className="flex items-center gap-3">
<svg
aria-hidden="true"
focusable="false"
viewBox="0 0 1 1"
className="size-10 shrink-0 overflow-visible"
>
<rect
width="1"
height="1"
fill="none"
stroke="var(--color-foundations-line-muted)"
strokeWidth="0.03"
/>
<path
d={`M0 1 C${x1} ${1 - y1} ${x2} ${1 - y2} 1 0`}
fill="none"
stroke="var(--color-foundations-primary-bg)"
strokeWidth="0.06"
/>
</svg>
<div className="flex min-w-0 grow flex-col gap-1">
<code className="text-small color-label">{name}</code>
<div className="relative mx-2 h-3 rounded-full bg-(--color-foundations-line-muted)">
<span
className={`absolute top-0 size-3 -translate-x-1/2 rounded-full bg-(--color-foundations-primary-bg) transition-[left] duration-1000 motion-reduce:transition-none ${name} ${end ? 'left-full' : 'left-0'}`}
/>
</div>
</div>
</div>
))}
</div>
<Button.button variant="secondary" onClick={() => setEnd(!end)}>
Play
</Button.button>
</div>
)
}For a transition, pair an ease-* class with Tailwind's transition-* and duration-* utilities. Hover the cards to compare a standard curve with an overshooting one:
export default function Demo() {
return (
<div className="grid w-full grid-cols-2 gap-4">
<div className="rounded-lg border border-(--color-foundations-surface-border) bg-(--color-foundations-surface-bg) p-6 transition-transform duration-300 ease-out-cubic hover:-translate-y-2">
<p className="text-h5 color-heading">ease-out-cubic</p>
<p className="text-small color-body">Hover to lift</p>
</div>
<div className="rounded-lg border border-(--color-foundations-surface-border) bg-(--color-foundations-surface-bg) p-6 transition-transform duration-300 ease-out-back hover:-translate-y-2">
<p className="text-h5 color-heading">ease-out-back</p>
<p className="text-small color-body">Hover to lift</p>
</div>
</div>
)
}<a class="transition-colors duration-300 ease-out-cubic hover:text-base-600" href="/pricing">Pricing</a>In your own CSSLink to this section
The --ease-* variables are declared on :root in the base layer, so they exist whether or not the matching utility appears in your markup. Use them when hand-written CSS has to match an easing a Systhema component uses, instead of copying the literal:
@layer app {
.drawer {
transition: transform 300ms var(--ease-out-cubic);
}
}Reduced motionLink to this section
CSS transitions are not switched off for you. Add Tailwind's motion-reduce:transition-none (or motion-safe: on the transition classes) to anything that moves, as the curve demo above does. Systhema's fade animations handle reduced motion themselves.
Responsive and state variantsLink to this section
ease-* takes every variant: hover:ease-out-back applies a different curve while hovered, md:ease-in-out-quint from md up.
CustomizingLink to this section
A project's easings token collection overrides the built-in curves one by one: a Figma curve named easeOutCubic replaces ease-out-cubic, animate-ease-out-cubic and --ease-out-cubic together, and any curve the collection doesn't declare keeps its built-in value. The same collection can declare durations, emitted as --duration-<name>, and a transition collection adds per-component --transition-* variables (the button reads them today). The default tokens ship curves only, so no --duration-* or --transition-* variable exists until your tokens declare one.
- Motion tokens explains both collections.
- The motion reference lists every curve with its token name.