Animate with attributes. Nothing else.

Compose entrances, springs and staggers directly in your markup. A tiny engine handles when — scroll, hover, click, focus — and pure CSS handles how.

$ bun add vivace-css
"@pr-i_child-ascend_ease-out-back @fd @sc-i!"
Gather
18
animation keys
5
trigger types
0
dependencies
< 7 kB
gzipped, CSS + JS

Eighteen keys. Hover to feel them.

full gallery
@fd fade@sl slide@rt rotate@sc scale@sp spiral@vb vibrate@pr perspective@bn bounce@sh shake@fl flick@bl blur@sw swing@pop pop@dr drop@sq squash@hb heartbeat@wv wave@gl glow

Two steps. No wrappers, no keyframes.

1

Initialize once

One call at your app's root. A MutationObserver keeps watching, so anything your framework mounts later registers itself.

ts
import Vivace from 'vivace-css'
import 'vivace-css/vivace.css'

Vivace.init()
2

Write attributes

Keys (@) say what plays, modifiers (_) tune it, data-viv-on says when. Everything composes.

html
<ul data-viv="@fd_child-ascend @sl-y" data-viv-on="appearing">
  <li>Every list item</li>
  <li>slides up in</li>
  <li>sequence. That's it.</li>
</ul>
  • Every list item
  • slides up in
  • sequence. That's it.

Small grammar, endless compositions

Keys stack in one attribute; modifiers concatenate onto the key they tune. Click a card to replay it, or open it in the playground.

Real triggers, zero listeners per element

load, appearing, hover, click and focus — one shared IntersectionObserver and a handful of delegated listeners for the whole page, no matter how many elements animate.

trigger reference
hover me
scrolled in

Beyond entrances

Exit animations that resolve as promises, bubbling lifecycle events (vivace:play, vivace:end), and a plugin API for your own keys and triggers — at runtime or as SCSS plugins.

await Vivace.out(el) — remove without a flash
events for chaining logic off animations
defineKey / defineTrigger, one-file SCSS plugins
prefers-reduced-motion built in — content never hides
extending vivace
ts
// exits that wait for themselves
await Vivace.out(toast)   // plays data-viv-out, resolves when done
toast.remove()            // fill-mode kept it hidden — no flash

// your own keys and triggers
Vivace.defineKey('@wb', { keyframe: 10, vars: { '--ARZ1': '8deg' } })
Vivace.defineTrigger('longpress', (el, fire) => { /* … */ })

Works where your markup lives

Attributes pass through every renderer, and dynamically mounted elements register themselves. No wrapper components to import, ever.

Compose one in ten seconds

Chain tokens in the playground, watch it live on real page sections, copy the attribute out — or ship it to the community gallery.

Portrait of Axel RAKOTOARIVAO
Created by
Axel RAKOTOARIVAO
MIT — keyframe DSL inspired by A.css
bun add vivace-css