Docs
Next

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

VariableValueClass
--ease-in-sinecubic-bezier(0.12, 0, 0.39, 0)ease-in-sine
--ease-out-sinecubic-bezier(0.61, 1, 0.88, 1)ease-out-sine
--ease-in-out-sinecubic-bezier(0.37, 0, 0.63, 1)ease-in-out-sine
--ease-in-quadcubic-bezier(0.11, 0, 0.5, 0)ease-in-quad
--ease-out-quadcubic-bezier(0.5, 1, 0.89, 1)ease-out-quad
--ease-in-out-quadcubic-bezier(0.45, 0, 0.55, 1)ease-in-out-quad
--ease-in-cubiccubic-bezier(0.32, 0, 0.67, 0)ease-in-cubic
--ease-out-cubiccubic-bezier(0.33, 1, 0.68, 1)ease-out-cubic
--ease-in-out-cubiccubic-bezier(0.65, 0, 0.35, 1)ease-in-out-cubic
--ease-in-quartcubic-bezier(0.5, 0, 0.75, 0)ease-in-quart
--ease-out-quartcubic-bezier(0.25, 1, 0.5, 1)ease-out-quart
--ease-in-out-quartcubic-bezier(0.76, 0, 0.24, 1)ease-in-out-quart
--ease-in-quintcubic-bezier(0.64, 0, 0.78, 0)ease-in-quint
--ease-out-quintcubic-bezier(0.22, 1, 0.36, 1)ease-out-quint
--ease-in-out-quintcubic-bezier(0.83, 0, 0.17, 1)ease-in-out-quint
--ease-in-expocubic-bezier(0.7, 0, 0.84, 0)ease-in-expo
--ease-out-expocubic-bezier(0.16, 1, 0.3, 1)ease-out-expo
--ease-in-out-expocubic-bezier(0.87, 0, 0.13, 1)ease-in-out-expo
--ease-in-circcubic-bezier(0.55, 0, 1, 0.45)ease-in-circ
--ease-out-circcubic-bezier(0, 0.55, 0.45, 1)ease-out-circ
--ease-in-out-circcubic-bezier(0.85, 0, 0.15, 1)ease-in-out-circ
--ease-in-backcubic-bezier(0.36, 0, 0.66, -0.56)ease-in-back
--ease-out-backcubic-bezier(0.34, 1.56, 0.64, 1)ease-out-back
--ease-in-out-backcubic-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:

Transition easing
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:

src/app/(site)/globals.css
@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.