Morphos & Kosmesis are here.Explore
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, and stay for the page lifetime. Identical CSS across different components shares one injection. Creating an instance only checks that its <style> is still in the document (putting it back if something removed it), so a stylesheet without @Param() costs each instance nothing more: every instance receives the same frozen class map, with no lifecycle hooks or signals.

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