PraxisJS

Keyframes

keyframes() — define scoped @keyframes animations with content-hashed names. No build step, no name collisions.

Keyframes

keyframes() defines a @keyframes animation, injects it into document.head, and returns a scoped name safe to use in any animation or animationName property.

The name is content-hashed — two identical animations always produce the same name and share one injection. Two different animations never collide.


Basic usage

import { keyframes, Stylesheet } from '@praxisjs/css'

const pulse = keyframes('pulse', {
  from: { opacity: 1 },
  to:   { opacity: 0.4 },
})

class LoaderStyles extends Stylesheet {
  $spinner = this.css({
    width: '24px',
    height: '24px',
    borderRadius: '50%',
    background: 'var(--accent)',
    animation: `${pulse} 1.2s ease-in-out infinite alternate`,
  })
}

The generated CSS:

@keyframes prx-pulse-a1b2c3 {
  from { opacity: 1; }
  to   { opacity: 0.4; }
}

Percentage stops

const bounce = keyframes('bounce', {
  '0%':   { transform: 'translateY(0)' },
  '40%':  { transform: 'translateY(-12px)' },
  '60%':  { transform: 'translateY(-6px)' },
  '100%': { transform: 'translateY(0)' },
})

class BounceStyles extends Stylesheet {
  $icon = this.css({ animation: `${bounce} 0.6s ease-in-out` })
}

Multiple keyframes per stylesheet

const fadeIn  = keyframes('fade-in',  { from: { opacity: 0 }, to: { opacity: 1 } })
const slideUp = keyframes('slide-up', { from: { transform: 'translateY(12px)', opacity: 0 }, to: { transform: 'translateY(0)', opacity: 1 } })
const spin    = keyframes('spin',     { from: { transform: 'rotate(0deg)' }, to: { transform: 'rotate(360deg)' } })

class ModalStyles extends Stylesheet {
  $overlay = this.css({ animation: `${fadeIn} 0.2s ease` })
  $panel   = this.css({ animation: `${slideUp} 0.3s ease` })
  $loading = this.css({ animation: `${spin} 0.8s linear infinite` })
}

Shared animations

keyframes() is safe to call at module level and share across multiple stylesheets. The injection is deduplicated by content hash.

// animations.ts — define once, import anywhere
export const fadeIn  = keyframes('fade-in',  { from: { opacity: 0 }, to: { opacity: 1 } })
export const slideUp = keyframes('slide-up', {
  from: { opacity: 0, transform: 'translateY(8px)' },
  to:   { opacity: 1, transform: 'translateY(0)' },
})

// button-styles.ts
import { fadeIn } from './animations'

class ButtonStyles extends Stylesheet {
  $ripple = this.css({ animation: `${fadeIn} 0.15s ease` })
}

// modal-styles.ts
import { slideUp } from './animations'

class ModalStyles extends Stylesheet {
  $panel = this.css({ animation: `${slideUp} 0.25s ease` })
}

Combining with @Style()

Use @Style() to make the animation duration or timing reactive:

import { keyframes, Style, Stylesheet, Styled } from '@praxisjs/css'

const spin = keyframes('spin', {
  from: { transform: 'rotate(0deg)' },
  to:   { transform: 'rotate(360deg)' },
})

class SpinnerStyles extends Stylesheet {
  $root = this.css({
    display: 'inline-block',
    width: '20px', height: '20px',
    borderRadius: '50%',
    border: '2px solid #e5e7eb',
    borderTopColor: 'var(--accent)',
    animation: `${spin} var(--speed, 0.8s) linear infinite`,
  })
}

@Component()
class Spinner extends StatefulComponent {
  @Style('--speed') speed = '0.8s'   // control animation speed reactively

  @Styled(SpinnerStyles) $s!: SpinnerStyles

  render() {
    return <span class={this.$s.$root} />
  }
}

SSR

When document is not available (server-side rendering), keyframes() skips the injection and still returns the scoped name. Inject the style server-side via your framework's head management.

Storybook
Live demo — keyframes() animations

On this page