Morphos & Kosmesis are here.Explore
PraxisJS

Reactive CSS

@Param() and @Style() — reactive CSS custom properties that update the DOM directly, without re-renders.

Reactive CSS

Two decorators let you bind signal values directly to CSS custom properties on DOM elements. Both update element.style directly — no class toggling, no re-render.


@Param() — vars inside a stylesheet

@Param() marks a field inside a ReactiveStylesheet subclass as a reactive CSS custom property. Setting the field on the injected stylesheet object calls element.style.setProperty('--fieldName', value) on the component's container element.

import { Param, ReactiveStylesheet, Styled } from '@praxisjs/css'
import { Component, State } from '@praxisjs/decorators'
import { StatefulComponent } from '@praxisjs/core'

class CardStyles extends ReactiveStylesheet {
  @Param() color  = '#3b82f6'   // → --color on the element
  @Param() radius = '8px'       // → --radius on the element

  $root  = this.css({ borderRadius: 'var(--radius)', border: '2px solid var(--color)' })
  $title = this.css({ color: 'var(--color)', fontWeight: 600 })
}

@Component()
class Card extends StatefulComponent {
  @Styled(CardStyles) $card!: CardStyles

  render() {
    return (
      <div class={this.$card.$root}>
        <h3 class={this.$card.$title}>Title</h3>
        <button onClick={() => { this.$card.color = '#ef4444' }}>Red</button>
        <button onClick={() => { this.$card.color = '#3b82f6' }}>Blue</button>
      </div>
    )
  }
}

Setting this.$card.color = '#ef4444' calls element.style.setProperty('--color', '#ef4444'). The class names in $root and $title stay the same — only the CSS var value changes.

Per-instance independence

Each component instance gets its own independent signal for each @Param() field. Two Card components on the same page have completely independent color and radius values.

Only inside ReactiveStylesheet

@Param() is constrained to ReactiveStylesheet subclasses at the TypeScript level. Using @Styled(ReactiveStylesheetSubclass) on a StatelessComponent field is also a compile-time error.

Storybook
Live demo — @Param() in action

@Style('--var') — vars on the component

@Style() is a field decorator that works like @State but also syncs the value to a CSS custom property directly on the component's container element.

The property name must start with --. Use it for component-wide theming variables that child elements consume via var(--name).

import { Style } from '@praxisjs/css'
import { Component } from '@praxisjs/decorators'
import { StatefulComponent } from '@praxisjs/core'

@Component()
class ThemeProvider extends StatefulComponent {
  @Style('--accent') accent = '#6d5bbd'
  @Style('--bg')     bg     = '#f8f7ff'
  @Style('--radius') radius = '8px'

  setTheme(accent: string, bg: string) {
    this.accent = accent
    this.bg     = bg
  }

  render() {
    return (
      <div style="padding: 20px; background: var(--bg);">
        <button
          style="background: var(--accent); border-radius: var(--radius);"
          onClick={() => this.setTheme('#ef4444', '#fef2f2')}
        >
          Red theme
        </button>
        {this.props.children}
      </div>
    )
  }
}

All child elements using var(--bg), var(--accent), or var(--radius) update instantly when setTheme() runs.

Cleanup on unmount

@Style removes the CSS property and stops the reactive effect when the component unmounts. The property is removed from the element it was set on at mount, even if the component's anchor has already been detached.

Storybook
Live demo — @Style()

@Param() vs @Style()

@Param()@Style()
Defined inReactiveStylesheet subclassComponent class
Accessed viathis.$card.colorthis.accent
CSS var--fieldName--fieldName
Component restrictionStatefulComponent only (via ReactiveStylesheet)Any component
Best forStyles tightly coupled to a @Styled classComponent-wide theming variables

Both set CSS custom properties on the container element — the difference is organizational.


Combining both

@Style() sets vars on the container; @Param() reads them in the class CSS. Together they separate theme control from class structure:

import { ReactiveStylesheet, Param, Style, Styled } from '@praxisjs/css'

class AlertStyles extends ReactiveStylesheet {
  $root = this.css({
    padding: '16px', borderRadius: 'var(--radius)',
    borderLeft: '4px solid var(--border)',
    background: 'var(--bg)',
  })
}

@Component()
class Alert extends StatefulComponent {
  // @Style controls the container vars — shared by all nested elements
  @Style('--bg')     bg     = '#eff6ff'
  @Style('--border') border = '#3b82f6'
  @Style('--radius') radius = '8px'

  @Styled(AlertStyles) $alert!: AlertStyles

  setVariant(bg: string, border: string) {
    this.bg     = bg
    this.border = border
  }

  render() {
    return <div class={this.$alert.$root}>{this.props.children}</div>
  }
}
Storybook
Live demo — @Styled + @Style combined

On this page