PraxisJS

Stylesheets

Stylesheet and ReactiveStylesheet — base classes for defining scoped CSS. @Styled injects class names into components.

Stylesheets

Styles are defined by extending Stylesheet (or ReactiveStylesheet) and declaring $-prefixed fields. Each field becomes a scoped class name when injected into a component via @Styled.


Stylesheet

Use when you only need static class names — no reactive CSS vars. Works on any component, including StatelessComponent.

import { Stylesheet } from '@praxisjs/css'

class BadgeStyles extends Stylesheet {
  $root    = this.css({ display: 'inline-flex', padding: '2px 10px', borderRadius: '99px' })
  $success = this.css({ background: '#f0fdf4', color: '#15803d' })
  $error   = this.css({ background: '#fef2f2', color: '#b91c1c' })
}

Plain template strings are also accepted:

class LabelStyles extends Stylesheet {
  $root = `font-size: 0.875rem; font-weight: 500; color: #374151;`
}

ReactiveStylesheet

Use when you need @Param() reactive CSS vars alongside class fields. See Reactive CSS for the full @Param() reference.

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

class CardStyles extends ReactiveStylesheet {
  @Param() color  = '#3b82f6'
  @Param() radius = '8px'

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

@Styled(ReactiveStylesheetSubclass) can only be applied to StatefulComponent fields — TypeScript enforces this at compile time.


@Styled(StyleClass) — injecting class names

@Styled is a field decorator that processes a Stylesheet subclass and injects a class name map into the component. Each $-prefixed field becomes a scoped class name string.

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

class CardStyles extends Stylesheet {
  $root     = this.css({ display: 'flex', gap: '12px', padding: '16px', borderRadius: '8px' })
  $selected = this.css({ border: '2px solid var(--accent)' })
  $title    = this.css({ fontSize: '1rem', fontWeight: 600 })
}

@Component()
class Card extends StatefulComponent {
  @State() selected = false

  @Styled(CardStyles)
  $card!: CardStyles

  render() {
    return (
      <div class={() => cx(this.$card.$root, { [this.$card.$selected]: this.selected })}>
        <h3 class={this.$card.$title}>Card</h3>
      </div>
    )
  }
}

On StatelessComponent

Plain Stylesheet (no @Param()) works on stateless components. Class names are static and injected once, shared across all instances.

import { Component } from '@praxisjs/decorators'
import { StatelessComponent } from '@praxisjs/core'

@Component()
class Badge extends StatelessComponent<{ label: string; variant?: 'success' | 'error' }> {
  @Styled(BadgeStyles) $badge!: BadgeStyles

  render() {
    const v = this.props.variant ?? 'success'
    return (
      <span class={cx(this.$badge.$root, this.$badge[`$${v}` as keyof BadgeStyles] as string)}>
        {this.props.label}
      </span>
    )
  }
}
Storybook
Live demo — @Styled on StatelessComponent

How scoping works

Each $-prefixed field gets a globally unique class name derived from a hash of its CSS content — for example prx-root-a1b2c3. Two fields with identical CSS share the same name. Class names are deterministic and stable.

Runtime mode (default / dev server): <style> elements are injected eagerly at decoration time (module load), before any component mounts. Each instance mount/unmount increments/decrements an internal reference count, but the base injection from decoration time is permanent — styles stay for the page lifetime. Identical CSS across different components shares one injection.

Static mode (production + praxisjsCSS() plugin): class names are identical but CSS is extracted at build time into virtual:praxisjs/styles.css. No <style> elements are injected at runtime — the CSS is part of the bundle from the start, fully cacheable by the browser. See Static extraction.


Multiple stylesheet fields

A component can hold as many Stylesheet fields as needed. Each one injects its own scoped stylesheet independently:

class LayoutStyles extends Stylesheet {
  $container = this.css({ display: 'grid', gridTemplateColumns: '1fr 3fr', gap: '24px' })
  $sidebar   = this.css({ padding: '16px', background: '#f9fafb' })
}

class TypographyStyles extends Stylesheet {
  $heading = this.css({ fontSize: '1.5rem', fontWeight: 700, lineHeight: 1.2 })
  $body    = this.css({ fontSize: '0.9375rem', lineHeight: 1.6, color: '#374151' })
}

@Component()
class Article extends StatefulComponent {
  @Styled(LayoutStyles)     $layout!: LayoutStyles
  @Styled(TypographyStyles) $type!: TypographyStyles

  render() {
    return (
      <div class={this.$layout.$container}>
        <aside class={this.$layout.$sidebar}>…</aside>
        <main>
          <h1 class={this.$type.$heading}>Title</h1>
          <p class={this.$type.$body}>…</p>
        </main>
      </div>
    )
  }
}
Storybook
Live demo — multiple @Styled fields

On this page