pyreon

@pyreon/ui-core is the foundation layer of the Pyreon UI system. It ships PyreonUI — a single provider that replaces the old three-provider split (theme + mode + config) — plus reactive mode resolution (useMode), an opt-in CSS-variables theming mode, the init() escape hatch for non-provider environments, and the zero-dependency utilities every other UI package builds on.

@pyreon/ui-corestable

Installation

npm install @pyreon/ui-core
bun add @pyreon/ui-core
pnpm add @pyreon/ui-core
yarn add @pyreon/ui-core

Quick Start

Wrap your app in a single PyreonUI provider. It wires the theme, the color mode, and the internal config context in one component — no separate init() call, no nested providers.

import { PyreonUI, useMode } from '@pyreon/ui-core'
import { enrichTheme } from '@pyreon/unistyle'

const theme = enrichTheme({
  colors: { primary: '#3b82f6', secondary: '#6366f1' },
  fonts: { body: 'Inter, sans-serif' },
})

const App = () => (
  <PyreonUI theme={theme} mode="system">
    <MyApp />
  </PyreonUI>
)

// useMode() reads the resolved mode reactively — "light" or "dark"
function ThemeBadge() {
  const mode = useMode()
  return <div class={mode() === 'dark' ? 'badge-dark' : 'badge-light'}>{mode()}</div>
}
Theme Mode Provider

Why a Single Provider?

Earlier Pyreon UI required three nested providers — one for the styler theme, one for the mode, one for the config context. PyreonUI collapses all three into one mount and calls init() internally, so consumers never wire the layers up by hand.

// ❌ The old split — three nested providers, manual init()
init({ styled, css })
;<ThemeProvider theme={theme}>
  <ModeProvider mode="dark">
    <ConfigProvider value={config}>
      <App />
    </ConfigProvider>
  </ModeProvider>
</ThemeProvider>

// ✅ One provider, zero init wiring
;<PyreonUI theme={theme} mode="dark">
  <App />
</PyreonUI>

The PyreonUI Provider

PyreonUI accepts three props plus children. All are optional except in practice you want at least a theme at the outermost provider.

interface PyreonUIProps {
  theme?: PyreonTheme
  mode?: 'light' | 'dark' | 'system' | (() => 'light' | 'dark' | 'system')
  inversed?: boolean
  children?: VNodeChild
}

theme

An enriched theme object. Always run a raw theme through enrichTheme() (re-exported from @pyreon/unistyle) first so it carries the default breakpoints, rootSize, and unit utilities the system expects.

import { enrichTheme } from '@pyreon/unistyle'

const theme = enrichTheme({
  colors: { primary: '#3b82f6' },
  spacing: { small: 8, medium: 16 },
})

;<PyreonUI theme={theme}>
  <App />
</PyreonUI>

When theme is omitted, the theme is inherited from the nearest ancestor PyreonUI — this is what makes a nested <PyreonUI inversed> work without re-passing the theme. At the outermost provider with no ancestor and no theme, styled descendants see theme fields as undefined (no crash, but no design tokens either) — so pass a real theme at the top.

mode

The color mode: 'light', 'dark', or 'system'. It can also be a signal or getter for reactive switching.

import { signal } from '@pyreon/reactivity'

// Static
;<PyreonUI theme={theme} mode="dark"></PyreonUI>

// Reactive signal / getter — toggling the signal re-resolves the mode
const mode = signal<'light' | 'dark' | 'system'>('light')
;<PyreonUI theme={theme} mode={mode}></PyreonUI>

// System — follows the OS preference (see below)
;<PyreonUI theme={theme} mode="system"></PyreonUI>

When mode is omitted, the resolved mode is inherited from the nearest ancestor PyreonUI (or 'light' at the root).

inversed

Flip the resolved mode for a nested section — e.g. a dark sidebar inside a light app. The flip is scoped: only descendants of that PyreonUI see the inverted mode; ancestors and siblings are unchanged.

<PyreonUI theme={theme} mode="light">
  <Header />
  {/* inherits the theme, inherits mode="light", then flips → "dark" */}
  <PyreonUI inversed>
    <DarkSidebar />
  </PyreonUI>
</PyreonUI>

When inversed is set without an explicit mode, the provider inherits the parent's mode and flips it (light → dark, dark → light).

mode="system" — OS Dark-Mode Detection

When mode="system", PyreonUI subscribes to matchMedia('(prefers-color-scheme: dark)') and resolves the mode against the OS preference. The subscription is lazy — created on first read — and a single document-lifetime listener serves every useMode() consumer. When the user changes their system setting, the resolved mode updates reactively and every reader re-runs.

<PyreonUI theme={theme} mode="system">
  <App />
</PyreonUI>

// Anywhere in the subtree:
const mode = useMode() // tracks the OS preference, reactive

The detection is SSR-safe: when there's no DOM (matchMedia unavailable), the system mode resolves to 'light' on the server. For correct first-paint mode under SSR, pair this with cssVariablesPrePaintScript() (CSS-variables mode) or stamp the mode on <html> server-side.

useMode() — Reading the Resolved Mode

useMode() returns the currently resolved mode as a reactive signal — 'light' or 'dark' (never 'system'; that's resolved away). It reflects the OS preference under mode="system" and any inversed flips applied by ancestors. The subscription is component-scoped: readers re-run only when the resolved mode actually changes.

import { useMode } from '@pyreon/ui-core'

function ThemeIcon() {
  const mode = useMode()
  // mode() returns "light" | "dark" — reactive, resolved
  return <span>{mode() === 'dark' ? '🌙' : '☀️'}</span>
}

CSS-Variables Theming Mode

init({ cssVariables: true }) opts into a ui-system-wide CSS-variables theming mode. Instead of resolving theme tokens to class names per component, the theme JSON is autogenerated into CSS custom properties (--px-*), and a dark/light flip becomes one attribute write on <html> — no re-resolution, no className churn, zero per-component JavaScript on the flip.

Enable it once at boot, before the first render:

import { init } from '@pyreon/ui-core'

init({ cssVariables: true })
// or with options:
init({ cssVariables: { prefix: 'px', attribute: 'data-theme' } })

What PyreonUI does under this flag:

  • Autogenerates --px-* custom properties from the enriched theme (themeToCssVars) and injects the :root block once per theme identity (SSR-aware — the block rides getStyleTag() / the stream flush on the server).

  • Provides a var-leaf theme tree — every eligible theme value becomes a var(--px-…) reference string that flows through the styler / unistyle value pipeline untouched.

  • The root PyreonUI writes the mode attribute to document.documentElement (so it sits at :root, where the var rules cascade from).

  • Nested / inversed providers render a layout-neutral display: contents wrapper carrying the mode attribute, scoping the override to their subtree via the CSS cascade.

  • Component-level mode(a, b) pairs (from rocketstyle) become hashed var pairs resolved by the data-theme attribute.

init({ cssVariables: true })

const App = () => (
  <PyreonUI theme={theme} mode="dark">
    <Dashboard />
  </PyreonUI>
)

// Toggling mode is now one documentElement attribute write — the cascade
// re-resolves every mode-pair var; component classNames never change.

CSS-variables config options

OptionTypeDefaultDescription
prefixstring'px'Custom-property prefix: --<prefix>-<path>
attributestring'data-theme'Attribute carrying the active mode on <html> / nested wrappers

resolveCssVariables() is the single accessor that returns the defaulted config ({ enabled, prefix, attribute }) — the same one PyreonUI and the rocketstyle mode-pair factory read.

FOUC Prevention — cssVariablesPrePaintScript()

Under cssVariables, the root PyreonUI keeps document.documentElement in sync after hydration via an effect — but on a server-rendered or large/streamed document, there's a window before that effect runs where the page can paint with the wrong mode (the classic dark-mode flash). cssVariablesPrePaintScript() builds a self-contained, blocking <head> script that sets the mode attribute on documentElement before first paint.

import { cssVariablesPrePaintScript } from '@pyreon/ui-core'

// In your document <head>, BEFORE the app bundle:
// <script>{cssVariablesPrePaintScript()}</script>

The script reads, in order:

  1. A persisted user toggle from localStorage (default key 'zero-theme', values 'light'/'dark'),

  2. else the OS prefers-color-scheme,

  3. else fallback (default 'light'),

and writes the attribute at :root — exactly where the var rules cascade from and where the root PyreonUI writes after hydration, so the two agree and there's no flash for mode="system" or a persisted toggle. It's dependency-free and try/catch-wrapped (a storage/matchMedia throw never blocks paint).

cssVariablesPrePaintScript({
  attribute: 'data-theme', // defaults to the resolved init({ cssVariables }) attribute
  storageKey: 'zero-theme',
  fallback: 'light',
})

init() — The Escape Hatch

PyreonUI calls init() internally, so you rarely call it directly. It's the escape hatch for environments where the provider tree is unavailable — tests, or SSR setups that bypass PyreonUI. It configures the CSS engine connector and the ui-system-wide flags.

import { init } from '@pyreon/ui-core'
import { styled, css, keyframes } from '@pyreon/styler'

init({
  styled,
  css,
  keyframes,
  cssVariables: true, // opt into CSS-variables theming
})

init() accepts a partial config:

FieldTypeDescription
cssCSSEngineConnector['css']The css tag from the CSS engine (@pyreon/styler)
styledStyledFunctionThe styled factory
keyframesCSSEngineConnector['keyframes']The keyframes factory
componentstring | HTMLTagsDefault Element host tag (default 'div')
textComponentstring | HTMLTagsDefault Text host tag (default 'span')
createMediaQueries(props) => Record<…>Optional media-query factory hook
cssVariablesboolean | CssVariablesConfigOpt-in CSS-variables theming (default false)
styleExtractionbooleanOpt-in Custom-Property Style Extraction for the styled/Element pipeline

Low-Level Context & Provider

PyreonUI is the right mount in almost every case. The raw context and the low-level Provider are exported for advanced/internal use.

import { Provider, context } from '@pyreon/ui-core'
  • context — the internal CoreContextValue reactive context ({ theme, mode, isDark, isLight }). Because it's a reactive context, useContext(context) returns a getter — call it to read.

  • Provider (CoreProvider) — the low-level theme provider. @internal / @deprecated; it warns in dev. Prefer PyreonUI.

Utilities

@pyreon/ui-core ships zero-dependency helpers used across the UI system. They're available to apps too.

Object Helpers

import { merge, omit, pick, get, set } from '@pyreon/ui-core'

merge({ a: 1 }, { b: 2 }) // { a: 1, b: 2 } — deep merge, plain-object recursion
omit({ a: 1, b: 2, c: 3 }, ['b']) // { a: 1, c: 3 }
pick({ a: 1, b: 2, c: 3 }, ['a']) // { a: 1 }
get({ a: { b: 1 } }, 'a.b') // 1 (dot/bracket paths; default value supported)
set({}, 'a.b', 1) // { a: { b: 1 } }

Comparison Helpers

import { isEqual, isEmpty } from '@pyreon/ui-core'

isEqual({ a: 1 }, { a: 1 }) // true — deep structural equality
isEmpty({}) // true
isEmpty([]) // true
isEmpty(null) // true
isEmpty({ a: 1 }) // false

Throttle

import { throttle } from '@pyreon/ui-core'

const onScroll = throttle(handleScroll, 100, { leading: true, trailing: true })
onScroll.cancel() // cancel a pending trailing invocation

leading and trailing both default to true. The returned function carries a .cancel() method that clears any pending trailing call.

Component Composition

import { compose, hoistNonReactStatics } from '@pyreon/ui-core'

// compose applies higher-order components RIGHT-TO-LEFT (rightmost runs first)
const Enhanced = compose(withAuth, withTheme, withLogger)(BaseComponent)
// equivalent to withAuth(withTheme(withLogger(BaseComponent)))

// hoistNonReactStatics copies non-framework statics from source to target
hoistNonReactStatics(WrappedComponent, OriginalComponent)

Stable Values

import { useStableValue } from '@pyreon/ui-core'

// Returns a referentially stable reference — the identity only changes
// when the value is no longer deeply equal to the previous one.
const stable = useStableValue(derivedConfig)

Slot & Component Helpers

import { render, resolveSlot, isPyreonComponent } from '@pyreon/ui-core'
  • render(content, attachProps?) — flexible element renderer: primitives and arrays pass through, component functions mount via h(), render props are called with attachProps, falsy values return null. Used by the bases to normalize slot content.

  • resolveSlot(value) — resolves a slot value inside a reactive accessor. A component-reference (beforeContent={Header}) mounts as h(Header, null); an anonymous reactive accessor (() => <Icon name={signal()} />) is called bare so its signal reads land in the enclosing reactive scope. Returns the resolved atom.

  • isPyreonComponent(value) — discriminates a component-reference from a plain reactive-accessor function (both are typeof === 'function'). Uses framework markers (IS_ROCKETSTYLE / PYREON__COMPONENT / pkgName) first, then a naming convention (explicit displayName, or a PascalCase .name).

HTML Tag Constants

@pyreon/ui-core exports the canonical HTML tag lists the Element / Text bases use for tag dispatching and prop filtering.

import { HTML_TAGS, HTML_TEXT_TAGS } from '@pyreon/ui-core'

// HTML_TAGS      — every valid HTML tag name
// HTML_TEXT_TAGS — tags that carry inline text content (span, p, label, …)

The matching types are HTMLTags, HTMLTextTags, HTMLElementAttrs, and HTMLTagAttrsByTag<T> (the attribute shape for a given tag).

API Reference

PyreonUI(props)

PropTypeDescription
themePyreonThemeEnriched theme object. Inherited from ancestor when omitted
mode'light' | 'dark' | 'system' | (() => ThemeModeInput)Color mode; 'system' follows OS preference. Inherited when omitted
inversedbooleanFlip the resolved mode for this subtree only
childrenVNodeChildSubtree

Exports

ExportTypeDescription
PyreonUIComponentUnified provider — theme + mode + config in one mount
useModeHookReturns Signal<'light' | 'dark'> — the reactive resolved mode
initFunctionEscape-hatch config: CSS engine connector + cssVariables/styleExtraction
configObjectThe configuration singleton (current engine wiring + flags)
resolveCssVariablesFunctionReturns the defaulted { enabled, prefix, attribute } config
cssVariablesPrePaintScriptFunctionBuilds the blocking <head> FOUC script for CSS-variables mode
ProviderComponent@internal/@deprecated low-level theme provider — prefer PyreonUI
contextContextInternal CoreContextValue reactive context
composeFunctionHOC composition (right-to-left)
renderFunctionFlexible element/slot renderer
resolveSlotFunctionResolve a slot value inside a reactive accessor
isPyreonComponentFunctionDiscriminate a component reference from a reactive accessor
hoistNonReactStaticsFunctionCopy non-framework statics between components
useStableValueHookReferentially stable value (identity changes only on deep inequality)
mergeFunctionDeep merge of plain objects
omitFunctionCopy without keys (descriptor-preserving)
pickFunctionCopy only keys (descriptor-preserving)
getFunctionGet nested value by dot/bracket path
setFunctionSet nested value by path (prototype-pollution-safe)
throttleFunctionThrottle a function (leading/trailing, .cancel())
isEqualFunctionDeep structural equality
isEmptyFunctionCheck if a value is empty
HTML_TAGSArrayAll valid HTML tag names
HTML_TEXT_TAGSArrayText-content HTML tags

Types

TypeDescription
PyreonUIPropsProps of PyreonUI
ThemeMode'light' | 'dark' (resolved mode)
ThemeModeInput'light' | 'dark' | 'system' (the mode prop input)
CoreContextValue{ theme, mode, isDark, isLight } — the internal context shape
CSSEngineConnectorShape of the CSS-in-JS engine (css, styled, keyframes)
CssVariablesConfig{ prefix?, attribute? } — CSS-variables options
ResolvedCssVariablesConfig{ enabled, prefix, attribute } — defaulted config
CssVariablesPrePaintOptions{ attribute?, storageKey?, fallback? } for the pre-paint script
BreakpointsBreakpoint size map (e.g. { sm: 576, md: 768 })
BreakpointKeysKey union of Breakpoints
HTMLTagsUnion of all valid HTML tag names
HTMLTextTagsUnion of text-content HTML tags
HTMLElementAttrsPer-tag attribute map
HTMLTagAttrsByTag<T>Attribute shape for a given tag T
RenderType of the render helper
IsEmptyType of the isEmpty helper

Key Features

  • PyreonUI({ theme, mode, inversed }) — single provider replacing the old theme + mode + config split; calls init() internally.

  • mode="system" auto-detects the OS preference via matchMedia and updates reactively, with one document-lifetime subscription shared across all useMode() readers.

  • useMode() returns the resolved 'light' \| 'dark' mode as a reactive signal, honoring system and inversed.

  • init({ cssVariables: true }) opts into CSS-variables theming — a dark/light flip is one documentElement attribute write with zero re-resolution or className churn.

  • cssVariablesPrePaintScript() builds the blocking <head> script that prevents the dark-mode flash before first paint.

  • init() is callable directly for tests and SSR-without-PyreonUI environments.

  • Zero-dependency utilities (get, set, merge, pick, omit, throttle, isEmpty, isEqual) and slot/composition helpers (render, resolveSlot, compose, hoistNonReactStatics, useStableValue).

  • HTML_TAGS / HTML_TEXT_TAGS constants drive Element / Text base tag dispatching.

UI Core