Extending

Vivace's keys and triggers are both open sets. Everything below composes with the shipped grammar — custom keys take modifiers, custom triggers work on any composition.

Exit animations

Entrances are declarative, but leaving the DOM needs a promise: play the exit, then remove the element. Declare the exit composition in data-viv-out (default @fd-o) and await Vivace.out():

ts
import Vivace from 'vivace-css'

// <div data-viv="@dr" data-viv-out="@fd-o @sl-y-o">…</div>
await Vivace.out(toast)   // plays the exit, resolves when done
toast.remove()            // fill-mode kept it hidden — no flash

Lifecycle events

Animated elements dispatch bubbling events you can hook anywhere:

EventFires
vivace:playwhen an animation is (re)started
vivace:outwhen an exit composition starts
vivace:endwhen an animation finishes
ts
el.addEventListener('vivace:end', () => {
  // bubbles — listen on the element, a container, or document
})

Custom keys at runtime

defineKey() registers a new @key without touching SCSS. You fill the same custom-property slots the shipped keys use — --ATX0..10 / --ATY* translate, --ASX*/--ASY* scale, --ARX/Y/Z* rotate, --AOP0/1 opacity, --AF0/1 filter:

ts
import Vivace from 'vivace-css'

// '@wb' — a little wobble, usable immediately in any data-viv
Vivace.defineKey('@wb', {
  keyframe: 10,        // 2 = simple in/out, 10 = spring/oscillation
  duration: 0.9,       // --AD base seconds
  vars: {
    '--ARZ1': '8deg',
    '--ARZ3': '-6deg',
    '--ARZ5': '3deg',
    '--ARZ7': '-1.5deg'
  }
})

Scale amplitudes with the spring table (var(--ASP0)var(--ASP9)) and the intensity level (var(--AL)) so _lv-* modifiers keep working on your key.

Custom triggers

defineTrigger() adds a new data-viv-on value. The setup runs once per element; call fire() whenever the animation should play, and return a teardown for Vivace.destroy():

ts
import Vivace from 'vivace-css'

// data-viv-on="longpress"
Vivace.defineTrigger('longpress', (el, fire) => {
  let timer: ReturnType<typeof setTimeout>
  const down = () => { timer = setTimeout(fire, 500) }
  const up = () => clearTimeout(timer)

  el.addEventListener('pointerdown', down)
  el.addEventListener('pointerup', up)
  return () => {
    el.removeEventListener('pointerdown', down)
    el.removeEventListener('pointerup', up)
  }
})

Register triggers before Vivace.init() so elements parsed on load pick them up.

Keys as SCSS plugins

For keys you ship with your bundle, write an SCSS plugin instead — one file in keys/, one @use line, and the engine needs zero registration:

scss
// keys/_wobble.scss
@use '../config' as c;
@use '../mixins' as m;

@include m.key('@wb') {
  --AN: #{c.$kf-10};
  @for $i from 0 through 9 {
    --ARZ#{$i}: calc(#{m.alt($i) * 20deg} * var(--ASP#{$i}));
  }
}

Custom bundles work the same way: write your own entry that @uses keyframes, base and only the key/modifier plugins you want.

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