PraxisJS

Global Styles

globalStyle() and preflight() — inject unscoped CSS once, at module level. Ideal for resets, @font-face, and base element rules.

globalStyle()

globalStyle() injects unscoped CSS into <head> exactly once. It receives a factory function that is passed css — the same builder function as this.css() in a Stylesheet class — and returns a CSSBuilder or a raw CSS string.

Use .on(selector, props) to target specific elements with the fluent builder:

import { globalStyle } from '@praxisjs/css'

globalStyle(css =>
  css({})
    .on('*, *::before, *::after', { boxSizing: 'border-box', margin: 0, padding: 0 })
    .on('body', { fontFamily: 'system-ui, sans-serif', lineHeight: 1.5 })
    .on('img, svg', { display: 'block', maxWidth: '100%' })
)

Pass a raw CSS string when you need selectors or at-rules that the builder can't express:

globalStyle(_css => `
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-var.woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
  }
`)

globalStyle(_css => `@layer reset, tokens, base, components, utilities;`)

Usage pattern

Call globalStyle() at module level in a dedicated file, then import it in your app entry:

// src/base-styles.ts
import { globalStyle } from '@praxisjs/css'

globalStyle(css =>
  css({})
    .on('*, *::before, *::after', { boxSizing: 'border-box', margin: 0, padding: 0 })
    .on('body', { fontFamily: 'system-ui', lineHeight: 1.5, color: 'var(--color-text)' })
    .on('button', { font: 'inherit', cursor: 'pointer', border: 'none', background: 'none' })
    .on('a', { color: 'inherit', textDecoration: 'none' })
)
// src/main.ts
import './base-styles'     // side-effect import — runs globalStyle() once
import { App } from './app'

Layering

Pass layer to wrap the CSS in a named @layer, so it can be ordered against other layered CSS (like Tailwind's @layer utilities) instead of always winning the cascade regardless of specificity:

globalStyle(css =>
  css({}).on('body', { fontFamily: 'system-ui', lineHeight: 1.5 }),
  { layer: 'base' }
)

Cascade layer priority follows the order layers first appear in the document — whichever layer is declared first ranks lowest. globalStyle() registers its layer at runtime via document.head.appendChild, so calling it before importing CSS from another layered system only reliably ranks below that system in dev mode, where both are JS-mediated. A production build often extracts the other system's CSS into a static <link> parsed before any JS runs, which can flip the order. See Coexisting with Tailwind for the robust fix — declaring the layer name up front in your own static CSS.


Deduplication

globalStyle() is content-hashed — calling a factory that produces the same CSS twice injects only one <style> element. Safe to call in modules that are imported from multiple places.


Static extraction

With the praxisjsCSS() Vite plugin, globalStyle() calls are extracted at build time. The CSS lives in the static bundle — no <style> element is injected at runtime in production.

Global styles appear before scoped class rules in the extracted CSS. This ensures resets apply before any component styles.


SSR

When document is not available (server-side rendering), globalStyle() is a no-op and does not throw. Inject global styles on the server via your framework's head management.


API

type GlobalStyleFactory = (css: (props: CSSProperties) => CSSBuilder) => CSSBuilder | string

interface GlobalStyleOptions {
  layer?: string | false
}

function globalStyle(factory: GlobalStyleFactory, options?: GlobalStyleOptions): void
ParameterTypeDescription
factoryGlobalStyleFactoryReceives css (= createCSSBuilder) and returns a CSSBuilder or raw CSS string.
options.layerstring | falseWraps the CSS in a named @layer. Omit or pass false to inject un-layered CSS (default).

For component-scoped styles, use @Styled(). For scoped @keyframes animations, use keyframes().


preflight()

preflight() injects an opinionated browser reset into <head> exactly once. It normalises browser defaults across elements — removes margins and padding, resets borders, makes replaced elements block-level, and sets sensible form and typography defaults.

import { preflight } from '@praxisjs/css'

preflight()

Inspired by the Tailwind CSS preflight reset, adapted to use standard system font stacks without any Tailwind-specific references.


What it resets

  • box-sizing: border-box on all elements — removes margin and padding
  • Resets default border to 0 solid
  • Sets line-height: 1.5, system font stack (ui-sans-serif, system-ui, sans-serif, …), and disables iOS font size adjustment on html/:host
  • Removes heading font sizes and weights (h1h6 inherit from parent)
  • Resets link colours and text decoration
  • Makes img, svg, video, canvas, audio, iframe, embed, object display: block; vertical-align: middle
  • Constrains images and videos to their parent width (max-width: 100%; height: auto)
  • Removes default list styles (ol, ul, menu)
  • Resets form element styles to inherit (font, color, letter-spacing, border-radius, background-color, opacity)
  • Normalises ::placeholder opacity in Firefox; sets semi-transparent placeholder colour in modern browsers
  • Removes default textarea horizontal resize

Usage

Call preflight() at the top of your app entry before mounting the root component:

// src/main.ts
import { preflight } from '@praxisjs/css'
import { App } from './app'

preflight()
mount(App, document.getElementById('app')!)

Like globalStyle(), preflight() is idempotent — calling it multiple times injects the CSS only once.


Coexisting with Tailwind (or other layered CSS)

preflight() wraps its reset in @layer reset by default. This lets it be ordered against other layered CSS — like Tailwind utilities, which live in @layer utilities — instead of always winning the cascade regardless of specificity.

Cascade layer priority follows the order layers first appear in the document: whichever layer is declared first ranks lowest. In dev mode, both preflight() and a JS-imported Tailwind stylesheet register their layers imperatively, via <style> tags injected in import order — so calling preflight() before importing Tailwind's CSS is enough to rank reset below it.

This breaks in a production build. Tailwind's CSS is typically extracted into a static <link rel="stylesheet"> that sits in the compiled HTML <head> and is parsed before any JS runs — while preflight() still registers its @layer reset {...} block at runtime, via document.head.appendChild. That flips the effective order: Tailwind's layers (theme, base, components, utilities) get registered first, and reset — introduced for the first time only once your JS runs — is appended after them, ranking above utilities and silently wiping out every Tailwind utility class. Import order alone is not reliable across dev and prod — this exact failure mode has shown up as buttons rendering as unstyled plain text in a production Storybook build that worked fine in dev.

The robust fix is to declare the layer order up front, in your own static CSS, before Tailwind's @import runs:

/* e.g. .storybook/tailwind-theme.css */
@layer reset;

@import "tailwindcss";
@import "tw-animate-css";

A bare @layer reset; statement (no block) doesn't inject any rules — it just registers the name. Because it's the first layer declaration in this file, reset is pinned as the lowest-priority layer before Tailwind's own @layer theme, base, components, utilities; (emitted by the @import below it) gets a chance to register first. This is order-independent of whichever side — Tailwind's static <link> or preflight()'s runtime <style> — finishes supplying its actual rules later: reset is already locked in below utilities either way.

Pass a string to layer to match the name you declared, or layer: false to opt back into un-layered CSS:

preflight({ layer: 'reset' })   // matches `@layer reset;` above (also the default)
preflight({ layer: false })

Wrapping the reset in a layer only fixes cascade order — it does not deduplicate rules. If Tailwind's own preflight is also loaded, you'll get two resets running; disable one of them (e.g. Tailwind's preflight corePlugin) to avoid conflicting resets.


With the Vite plugin

preflight() is extracted at build time by praxisjsCSS() just like any other globalStyle() call. The reset CSS appears at the top of the static bundle, before all scoped component styles.


API

interface PreflightOptions {
  layer?: string | false
}

function preflight(options?: PreflightOptions): void
ParameterTypeDescription
options.layerstring | falseNames the CSS @layer the reset is wrapped in. Defaults to "reset". Pass false to inject un-layered CSS instead.

On this page