Skip to content

Transitions

Transition utilities animate a change of value, such as a background colour on hover, instead of switching at once. One class chooses which properties animate and brings a default duration and easing; optional classes change the duration, the easing and the delay.

  • Property classes. transition animates every property; transition-colors, transition-opacity, transition-shadow and transition-transform animate one group. transition-none turns transitions off.
  • Defaults included. Each property class also sets the default duration (200, 200ms) and easing (in-out), so one class is enough.
  • Overridable defaults. The defaults sit inside :where(), which has no specificity, so duration-* and ease-* override them wherever they appear in the stylesheet.
  • Tokens. Durations output var(--nu-duration-{key}) and easings var(--nu-ease-{key}). Delays are plain times, not tokens.
Class Animates
transition all
transition-colors color, background-color, border-color, text-decoration-color, fill, stroke
transition-opacity opacity
transition-shadow box-shadow
transition-transform transform
transition-none Nothing (transition-property: none)

Hover each button. The colour change animates over the default 200ms.

transition-colors
transition-shadow
transition-opacity
No transition
<div class="flex wrap gap-3">
<div class="transition-colors p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">transition-colors</div>
<div class="transition-shadow p-3 rounded border shadow-sm hover:shadow-lg cursor-pointer">transition-shadow</div>
<div class="transition-opacity p-3 rounded bg-brand-subtle opacity-50 hover:opacity-100 cursor-pointer">transition-opacity</div>
<div class="p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">No transition</div>
</div>

Prefer a specific property class over transition. Animating all also animates properties you did not mean to animate, such as a width that changes when the layout reflows.

The class CSS keeps the defaults in a separate :where() rule:

.transition {
transition-property: all;
}
:where(.transition, .transition-colors, .transition-opacity, .transition-shadow, .transition-transform) {
transition-duration: var(--nu-duration-200);
transition-timing-function: var(--nu-ease-in-out);
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
}

A rule inside :where() has a specificity of zero, so any class that sets transition-duration or transition-timing-function wins over the default, even one emitted earlier in the stylesheet.

KeyTokenValue
0--nu-duration-00ms
75--nu-duration-7575ms
100--nu-duration-100100ms
150--nu-duration-150150ms
200--nu-duration-200200ms
300--nu-duration-300300ms
500--nu-duration-500500ms
700--nu-duration-700700ms
1000--nu-duration-10001000ms
duration-75
duration-300
duration-1000
<div class="flex wrap gap-3">
<div class="transition-colors duration-75 p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">duration-75</div>
<div class="transition-colors duration-300 p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">duration-300</div>
<div class="transition-colors duration-1000 p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">duration-1000</div>
</div>
KeyTokenValue
linear--nu-ease-linearlinear
in--nu-ease-incubic-bezier(0.4, 0, 1, 1)
out--nu-ease-outcubic-bezier(0, 0, 0.2, 1)
in-out--nu-ease-in-outcubic-bezier(0.4, 0, 0.2, 1)
Key Use
linear Constant speed: progress bars, spinners, colour fades
in Starts slow, ends fast: elements leaving the screen
out Starts fast, ends slow: elements entering the screen
in-out Slow at both ends: elements moving within the screen (the default)
linear
in
out
in-out
Hover this panel
<div class="group space-y-2 p-3 bg-alternate rounded cursor-pointer">
<div class="transition-transform duration-1000 ease-linear group-hover:translate-x-20 w-16 p-2 bg-brand text-on-brand rounded">linear</div>
<div class="transition-transform duration-1000 ease-in group-hover:translate-x-20 w-16 p-2 bg-brand text-on-brand rounded">in</div>
<div class="transition-transform duration-1000 ease-out group-hover:translate-x-20 w-16 p-2 bg-brand text-on-brand rounded">out</div>
<div class="transition-transform duration-1000 ease-in-out group-hover:translate-x-20 w-16 p-2 bg-brand text-on-brand rounded">in-out</div>
<div class="text-sm text-muted">Hover this panel</div>
</div>

Delay classes wait before the transition starts. Keys come from $delays and give plain times: delay-150 is transition-delay: 150ms. There is no delay token.

Class Value
delay-0, delay-75, delay-100, delay-150, delay-200 0ms to 200ms
delay-300, delay-500, delay-700, delay-1000 300ms to 1000ms
delay-0
delay-300
delay-700
<div class="flex wrap gap-3">
<div class="transition-colors delay-0 p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">delay-0</div>
<div class="transition-colors delay-300 p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">delay-300</div>
<div class="transition-colors delay-700 p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">delay-700</div>
</div>

A delay applies both when the change starts and when it reverses. Keep delays short on hover effects, or the interface feels slow.

Every class on this page has a responsive variant with an @{breakpoint} suffix: transition-none@md, duration-300@lg. Variants apply from their breakpoint upwards.

duration-1000 duration-150@md: slow on small screens, quick from md
<div class="transition-colors duration-1000 duration-150@md p-3 rounded bg-alternate hover:bg-brand hover:text-white cursor-pointer">
duration-1000 duration-150@md: slow on small screens, quick from md
</div>
SuffixMedia query
@xs@media (min-width: 480px)
@sm@media (min-width: 640px)
@md@media (min-width: 768px)
@lg@media (min-width: 1024px)
@xl@media (min-width: 1280px)
@2xl@media (min-width: 1536px)

Transition classes have no state variants: a transition is set on the element’s resting state and applies to every change.

Each class has a mixin. duration(), ease() and delay() take a key or a raw value.

@use "@nuvoui/core" as *;
.button {
@include apply(transition-colors, duration(150), ease(out), bg(brand), bg(brand-hover) hover);
}
.button {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-duration: var(--nu-duration-200);
transition-timing-function: var(--nu-ease-in-out);
transition-duration: var(--nu-duration-150);
transition-timing-function: var(--nu-ease-out);
background-color: var(--nu-brand);
}
.button:hover {
background-color: var(--nu-brand-hover);
}

The property mixins write the default duration and easing as normal declarations. Include duration() or ease() after them to change one: the later declaration wins.

Mixin Output
transition transition-property: all plus the default duration and easing
transition-colors, transition-opacity, transition-shadow, transition-transform Their properties plus the default duration and easing
transition-none transition-property: none
duration($time) transition-duration
ease($name), ease-linear, ease-in, ease-out, ease-in-out transition-timing-function
delay($time) transition-delay
Mixin Accepts
duration() A $durations key (gives the token), a time in s or ms, calc() or var()
delay() A $delays key (gives the time), a time in s or ms, calc() or var()
ease() A $timing-functions key (gives the token), cubic-bezier(), steps(), linear(), var() or a CSS keyword (ease, ease-in, ease-out, ease-in-out, step-start, step-end)

Keys win over keywords: ease(linear) gives var(--nu-ease-linear), while ease(ease-in) is the CSS keyword ease-in, not the in key.

.sheet {
@include apply(transition-transform, duration(427ms), ease(cubic-bezier(0.34, 1.56, 0.64, 1)));
}

To use a timing function in classes and tokens, add it to $timing-functions. The map replaces the default map, so include the keys you keep. A new key gets a class (.ease-spring), a token (--nu-ease-spring) and works in ease(spring).

@use "@nuvoui/core" with (
$generate-utility-classes: true,
$timing-functions: (
"linear": linear,
"in": cubic-bezier(0.4, 0, 1, 1),
"out": cubic-bezier(0, 0, 0.2, 1),
"in-out": cubic-bezier(0.4, 0, 0.2, 1),
"spring": cubic-bezier(0.34, 1.56, 0.64, 1)
)
);
.drawer {
transition: transform token(duration-300) token(ease-out);
}
Variable Default Purpose
$durations 0, 75, 100, 150, 200, 300, 500, 700, 1000 (ms) Duration keys, classes and tokens
$delays Same keys and values as $durations Delay keys and classes
$timing-functions linear, in, out, in-out Easing keys, classes and tokens
$transition-default-duration "200" Duration key used by the property classes and mixins
$transition-default-ease "in-out" Easing key used by the property classes and mixins

The defaults must be keys of their maps. They also apply to hover-ready and the tooltips. The base button and field styles read the keys 200 and in-out directly, so keep those keys in custom maps while the base feature flag is on.

@use "@nuvoui/core" with (
$transition-default-duration: "150",
$transition-default-ease: "out"
);

Turn the classes off with the transitions feature flag. Tokens and mixins stay available.

Input Error
duration(250) Unknown duration: 250. Use a key (0, 75, 100, 150, 200, 300, 500, 700, 1000), a time in s or ms, calc() or var().
duration(fast) Unknown duration: fast. Use a key (0, 75, 100, 150, 200, 300, 500, 700, 1000), a time in s or ms, calc() or var().
delay(250) Unknown delay: 250. Use a key (0, 75, 100, 150, 200, 300, 500, 700, 1000), a time in s or ms, calc() or var().
ease(bounce) Unknown timing function: bounce. Use a key (linear, in, out, in-out), cubic-bezier(), steps(), linear(), var() or a keyword (linear, ease, ease-in, ease-out, ease-in-out, step-start, step-end).
token(delay-150) token(delay-150): unknown token. Valid names are the --nu- custom properties in :root, without --nu-.
$timing-functions without in-out $transition-default-ease: in-out is not a configured key. Valid keys: ...
Variable Description
--nu-duration-{key} One token per key of $durations.
--nu-ease-{key} One token per key of $timing-functions.

Delays have no tokens. All transition tokens are fixed across themes.

Generated from the compiled core build. Responsive variants are not listed.

Property (6 rules)
SelectorDeclarationsDefault value
.transition-nonetransition-property: nonenone
.transitiontransition-property: allall
.transition-colorstransition-property: color, background-color, border-color, text-decoration-color, fill, strokecolor, background-color, border-color, text-decoration-color, fill, stroke
.transition-opacitytransition-property: opacityopacity
.transition-shadowtransition-property: box-shadowbox-shadow
.transition-transformtransition-property: transformtransform
Duration (9 rules)
SelectorDeclarationsDefault value
.duration-0transition-duration: var(--nu-duration-0)0ms
.duration-75transition-duration: var(--nu-duration-75)75ms
.duration-100transition-duration: var(--nu-duration-100)100ms
.duration-150transition-duration: var(--nu-duration-150)150ms
.duration-200transition-duration: var(--nu-duration-200)200ms
.duration-300transition-duration: var(--nu-duration-300)300ms
.duration-500transition-duration: var(--nu-duration-500)500ms
.duration-700transition-duration: var(--nu-duration-700)700ms
.duration-1000transition-duration: var(--nu-duration-1000)1000ms
Easing (4 rules)
SelectorDeclarationsDefault value
.ease-lineartransition-timing-function: var(--nu-ease-linear)linear
.ease-intransition-timing-function: var(--nu-ease-in)cubic-bezier(0.4, 0, 1, 1)
.ease-outtransition-timing-function: var(--nu-ease-out)cubic-bezier(0, 0, 0.2, 1)
.ease-in-outtransition-timing-function: var(--nu-ease-in-out)cubic-bezier(0.4, 0, 0.2, 1)
Delay (9 rules)
SelectorDeclarationsDefault value
.delay-0transition-delay: 0ms0ms
.delay-75transition-delay: 75ms75ms
.delay-100transition-delay: 100ms100ms
.delay-150transition-delay: 150ms150ms
.delay-200transition-delay: 200ms200ms
.delay-300transition-delay: 300ms300ms
.delay-500transition-delay: 500ms500ms
.delay-700transition-delay: 700ms700ms
.delay-1000transition-delay: 1000ms1000ms

The default duration and easing rule, :where(.transition, .transition-colors, .transition-opacity, .transition-shadow, .transition-transform), is not listed above because it is not a class selector.

  • Reduced motion. The reset sets transition-duration: 0.01ms !important under prefers-reduced-motion: reduce, so every transition finishes at once for users who ask for less motion. Because the reset is a cascade layer, its !important wins over utility classes.
  • Do not hide meaning in motion. A state change must be clear without the animation, for example by a colour, an icon or text.
  • Keep it short. Interface transitions read best between 100ms and 300ms. Long durations and delays make controls feel unresponsive.
  • RTL. Durations, easing and delays do not depend on direction. A transition on a transform animates the transform as written; mirror the transform itself for right-to-left layouts.