Animations
Animation utilities run a keyframe animation on an element: a loading pulse, a spinner, a notification ping, an entrance fade or an error shake. Each class is a preset of an animation mixin; the mixins take a map of options for duration, distance, easing and repetition, and write their @keyframes once per stylesheet.
How animations work
Section titled “How animations work”- Classes are presets.
animate-spin,animate-pulseand the other classes call their mixin with the default options. - Mixins take a map.
animate-spin((duration: 0.8s))changes one option and keeps the defaults for the rest. - Keyframes once. Each mixin writes the
animationdeclaration and an@keyframesrule. The keyframes name is built from the options, so the same options share one rule and different options get their own. - Plain times. Animation durations are written as given (
1s,0.5s). They are not the--nu-duration-*tokens, but a mixin acceptstoken(duration-700)as a duration.
| Class | Animation | Duration | Repeats |
|---|---|---|---|
animate-pulse |
Opacity from 1 to 0.5 and back |
1s |
Forever |
animate-spin |
One full clockwise turn | 1s, linear |
Forever |
animate-ping |
Scales to 2 while fading out |
1s |
Forever |
animate-fade-in |
Opacity 0 to 1 |
0.5s |
Once, keeps the end state |
animate-fade-out |
Opacity 1 to 0 |
0.5s |
Once, keeps the end state |
animate-fade-in-up |
Fades in while rising 20px |
0.5s |
Once, keeps the end state |
animate-fade-in-down |
Fades in while dropping 20px |
0.5s |
Once, keeps the end state |
animate-shake |
Moves 5px left and right |
0.5s |
Once |
hover-ready |
Not an animation: a transition on all properties with the default duration and easing | 200ms |
Looping animations
Section titled “Looping animations”Pulse, spin and ping repeat forever. Use them for states that last an unknown time, such as loading or a live status.
<div class="flex wrap x-center gap-8 p-4 bg-alternate rounded"> <div class="animate-pulse w-24 p-3 bg-brand-subtle rounded">Loading</div> <div class="animate-spin w-8 h-8 bg-brand rounded"></div> <div class="relative w-4 h-4"> <div class="animate-ping absolute inset-0 rounded-full bg-brand"></div> <div class="relative w-4 h-4 rounded-full bg-brand"></div> </div></div>.skeleton { @include animate-pulse((min-opacity: 0.4, duration: 1.5s));}.spinner { @include animate-spin((duration: 0.8s));}.status-dot { @include animate-ping((scale: 3));}Entrance and exit
Section titled “Entrance and exit”The fade classes run once and keep their last frame (animation-fill-mode: forwards), so an element that fades out stays hidden. It still takes up space and still receives clicks; hide or remove it once the animation ends.
<div class="flex wrap gap-4 p-4 bg-alternate rounded"> <div class="animate-fade-in p-3 bg-surface border rounded">animate-fade-in</div> <div class="animate-fade-in-up p-3 bg-surface border rounded">animate-fade-in-up</div> <div class="animate-fade-in-down p-3 bg-surface border rounded">animate-fade-in-down</div></div>.modal { @include animate-fade((type: in-up, duration: 0.3s));}The examples run once when the page loads. Reload the page to see them again.
animate-shake moves the element left and right once. Use it to draw attention to a field that failed validation, together with a visible error message.
<div class="p-4 bg-alternate rounded"> <div class="animate-shake p-3 bg-surface border rounded">animate-shake</div></div>.input-invalid { @include animate-shake((intensity: 8px));}Bounce
Section titled “Bounce”Bounce has no class. animate-bounce() moves the element to an offset and back, forever. The default is 0.5rem down.
.scroll-hint { @include animate-bounce((vertical: 10px, duration: 2s));}Hover ready
Section titled “Hover ready”hover-ready is a transition helper: it sets a transition on all properties with the default duration and easing tokens, so any hover change on the element animates.
.hover-ready { transition-property: all; transition-duration: var(--nu-duration-200); transition-timing-function: var(--nu-ease-in-out);}For new code, prefer a specific transition class such as transition-colors or transition-transform; see Transitions.
Responsive variants
Section titled “Responsive variants”The eight animate-* classes have responsive variants with an @{breakpoint} suffix, applied from the breakpoint upwards: animate-spin@md. hover-ready has none. Animation classes have no state variants.
| Suffix | Media 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) |
Call the animation mixins with @include. Each takes one optional map; keys you leave out keep their defaults.
@use "@nuvoui/core" as *;
.spinner { @include animate-spin((duration: 0.8s));}.spinner { animation: anim-spin-normal 0.8s linear infinite;}@keyframes anim-spin-normal { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}Options
Section titled “Options”| Mixin | Options and defaults | Keyframes name |
|---|---|---|
animate-bounce |
vertical: 0.5rem, horizontal: 0, duration: 1s, timing: ease-in-out, iteration: infinite |
anim-bounce-0-0_5rem |
animate-pulse |
min-opacity: 0.5, max-opacity: 1, duration: 1s, timing: ease-in-out, iteration: infinite |
anim-pulse-50-100 |
animate-spin |
direction: normal, duration: 1s, timing: linear, iteration: infinite |
anim-spin-normal |
animate-ping |
scale: 2, duration: 1s, timing: cubic-bezier(0, 0, 0.2, 1), iteration: infinite |
anim-ping-20 |
animate-fade |
type: in, duration: 0.5s, timing: ease-in-out, iteration: 1 forwards |
anim-fade-in |
animate-shake |
intensity: 5px, duration: 0.5s, timing: ease-in-out, iteration: 1 |
anim-shake-5px |
animate-spin: anydirectionother thannormalturns anticlockwise (direction: reversegivesanim-spin-reverse).animate-fade:typeisin,out,in-uporin-down.animate-pulse: write the opacities as numbers between0and1.animate-bounceandanimate-shake: write distances with a unit.iterationis written after the easing in theanimationshorthand, so it can also carry a fill mode or a direction, as the fade default1 forwardsdoes.- Unknown option keys are ignored.
.badge-new { @include animate-ping((scale: 1.5, iteration: 3));}.nudge { @include animate-bounce((vertical: 0, horizontal: 4px, duration: token(duration-700)));}Custom keyframes
Section titled “Custom keyframes”ensure-keyframes($name) writes an @keyframes rule the first time a name is used and nothing on later calls, so a mixin that is included many times emits its keyframes once.
@mixin toast-in { animation: toast-in 0.3s ease-out; @include ensure-keyframes(toast-in) { from { opacity: 0; } to { opacity: 1; } }}
.toast { @include toast-in;}.snackbar { @include toast-in;}Animations and transform utilities
Section titled “Animations and transform utilities”Spin, ping, bounce, fade in up, fade in down and shake animate the transform property directly. While one runs, it replaces the transform written by the transform utilities, so rotate-45 animate-spin spins from 0deg, not from 45deg. To combine them, put the animation on a wrapper element and the transform on its child.
Configuration
Section titled “Configuration”Turn the classes off with the animations feature flag. The mixins stay available.
@use "@nuvoui/core" with ( $generate-utility-classes: true, $feature-flags: ("animations": false));hover-ready reads $transition-default-duration and $transition-default-ease; see Transitions.
Errors
Section titled “Errors”| Input | Error |
|---|---|
animate-fade((type: slide)) |
animate-fade(): unknown type slide. Use in, out, in-up or in-down. |
// error.panel { @include animate-fade((type: slide));}Class reference
Section titled “Class reference”Generated from the compiled core build. Responsive variants are not listed.
Animations (8 rules)
| Selector | Declarations | Default value |
|---|---|---|
.animate-pulse | animation: anim-pulse-50-100 1s ease-in-out infinite | anim-pulse-50-100 1s ease-in-out infinite |
.animate-spin | animation: anim-spin-normal 1s linear infinite | anim-spin-normal 1s linear infinite |
.animate-ping | animation: anim-ping-20 1s cubic-bezier(0, 0, 0.2, 1) infinite | anim-ping-20 1s cubic-bezier(0, 0, 0.2, 1) infinite |
.animate-fade-in | animation: anim-fade-in 0.5s ease-in-out 1 forwards | anim-fade-in 0.5s ease-in-out 1 forwards |
.animate-fade-out | animation: anim-fade-out 0.5s ease-in-out 1 forwards | anim-fade-out 0.5s ease-in-out 1 forwards |
.animate-fade-in-up | animation: anim-fade-in-up 0.5s ease-in-out 1 forwards | anim-fade-in-up 0.5s ease-in-out 1 forwards |
.animate-fade-in-down | animation: anim-fade-in-down 0.5s ease-in-out 1 forwards | anim-fade-in-down 0.5s ease-in-out 1 forwards |
.animate-shake | animation: anim-shake-5px 0.5s ease-in-out 1 | anim-shake-5px 0.5s ease-in-out 1 |
Hover ready (1 rules)
| Selector | Declarations | Default value |
|---|---|---|
.hover-ready | transition-property: all | all |
Accessibility and RTL
Section titled “Accessibility and RTL”-
Reduced motion. The reset contains this rule, which is emitted with core and has no feature flag:
@media (prefers-reduced-motion: reduce) {*,*::before,*::after {animation-duration: 0.01ms !important;animation-iteration-count: 1 !important;transition-duration: 0.01ms !important;scroll-behavior: auto !important;}}A user who asks for less motion sees each animation finish at once and run only once: a fade lands on its end state, and a spinner or a pulse stops. Because the reset is a cascade layer, its
!importantwins over utility classes and unlayered CSS. -
Do not rely on motion alone. A stopped spinner must still read as loading. Pair it with text, such as a visually hidden “Loading” label or
aria-busy="true"on the region that is updating. -
Custom reduced motion styles. The reset applies to every animation, including your own, so a replacement animation under reduced motion is also cut short. Give a static alternative instead with the
reduced-motionmixin, which wraps its content in@media (prefers-reduced-motion: reduce):.spinner {@include animate-spin;@include reduced-motion {animation: none;opacity: 0.6;}} -
Flashing. Keep looping animations slow and low in contrast. Content must not flash more than three times per second (WCAG 2.3.1).
-
Pause what loops. A looping animation that starts on its own and lasts more than five seconds next to other content needs a way to pause, stop or hide it (WCAG 2.2.2).
-
RTL. Animations do not depend on direction. Bounce, shake and the fades move on the physical axes, and spin turns clockwise in both directions.