Styling & Theming
Pyreon's UI system layers cleanly: styler (CSS-in-JS primitives) → unistyle (responsive props) → rocketstyle (multi-dimensional themed components) → PyreonUI (the unified theme + mode provider). Theme is a reactive context, so a whole-theme swap re-resolves CSS and swaps class names without remounting.
When to use what
One-off styled element →
styled('div')from@pyreon/styler.Responsive values (mobile-first arrays / breakpoint objects) →
@pyreon/unistyle.A reusable component with
state/size/variant/ dark-mode dimensions →@pyreon/rocketstyle.App-wide theme + light/dark → wrap once in
PyreonUI.
styler — CSS-in-JS
import { styled, css, keyframes } from '@pyreon/styler'
const Button = styled('button')`
padding: 8px 16px;
background: ${(t) => t.theme.colors.primary};
border-radius: 4px;
`styled('tag') returns a component; css (a tagged template) is a lazy CSSResult; keyframes returns an animation name string. useTheme() returns a theme snapshot; useThemeAccessor() returns the raw () => Theme accessor for tracking inside effects.
Dynamic, signal-driven styling, live:
unistyle — responsive props
Inside a unistyle-processed style object (a rocketstyle .theme(), for example), a value can be a single value, a mobile-first array, or a breakpoint object:
// padding: 8px on xs and sm, 16px from md up
.theme(() => ({ padding: ['8px', '8px', '16px'] }))
.theme(() => ({ padding: { xs: '8px', md: '16px' } }))Live breakpoint detection:
rocketstyle — multi-state components
Dimensions (state, size, variant, theme, dark mode) map prop values to CSS. Layout props go in .attrs(); CSS goes in .theme(). The default is useBooleans: false — dimension props take string values:
import { rocketstyle } from '@pyreon/rocketstyle'
import { Element } from '@pyreon/elements'
// rocketstyle(config?) returns a factory; call it with { name, component }.
const Button = rocketstyle()({ name: 'Button', component: Element })
.attrs({ tag: 'button' }) // layout: tag, direction, gap…
.states((t) => ({
primary: { backgroundColor: t.colors.primary, color: 'white' },
danger: { backgroundColor: t.colors.danger, color: 'white' },
}))
.sizes(() => ({
small: { padding: '4px 8px' },
large: { padding: '12px 24px' },
}))
// usage — string values, not booleans:
<Button state="primary" size="large">Save</Button>:hover / :focus / :active / :disabled are written as objects inside .theme() callbacks. Opt into vitus-labs-style boolean shorthand with rocketstyle({ useBooleans: true }).
Theming with PyreonUI
import { PyreonUI } from '@pyreon/ui-core'
<PyreonUI theme={myTheme} mode="system">
<App />
</PyreonUI>mode is "light" | "dark" | "system" (system auto-detects prefers-color-scheme); useMode() returns the resolved mode ('light' | 'dark') as a plain value — call it inside a reactive scope (a JSX accessor, an effect) to track changes. Opt into CSS-variables mode with init({ cssVariables: true }) — then a dark/light flip is a single data-theme attribute write with zero className churn.
Common pitfalls
Putting CSS in
.attrs()or layout in.theme(). Layout (tag,direction,gap,alignX/Y,block) belongs in.attrs(); colors/spacing/borders/pseudo-states belong in.theme().Booleans when
useBooleansis false (the default).<Button primary />is silently dropped — write<Button state="primary" />.CSS-spec property order. Unistyle uses property-first names:
borderWidthTop, notborderTopWidth.Re-augmenting the theme interface twice. If a theme library you depend on already augments
ThemeDefault/StylesDefault, a second app-level augmentation with a different type triggers TS2320. Remove the app-level one.