Styling Components

Customize component appearance: xstyle, Tailwind, className, rest props, compound components, data attribute selectors, tokens, and StyleX setup.

Overview

Style components with xstyle (StyleX), className (Tailwind or your own CSS), or a styling library aliased to Astryx tokens. All of them resolve to the same tokens.

ApproachUse forExample
StyleXComponent-specific overrides, reusable styles, pseudo-classes, and typed tokensconst styles = stylex.create(...); <Button xstyle={styles.save} />
Tailwind utilitiesPage layout, wrappers, and utility stylingclassName="flex gap-3 p-4"
classNameIntegrating with external CSS or Tailwind on componentsclassName="my-card shadow-lg"
Styling-library token aliasesKeeping Panda, Chakra, MUI, Emotion, styled-components, UnoCSS, CSS Modules, or Sass in sync with the systemcolors.surface = 'var(--color-background-surface)'

Theming and dark mode work whichever you choose. For external styling libraries, see astryx docs styling-libraries; it covers Tailwind, StyleX, Panda, Chakra, MUI, CSS-in-JS, CSS Modules, Sass, and useTheme() for non-CSS processing.

xstyle Prop

Every component accepts an xstyle prop for style customization. It accepts StyleX styles created via stylex.create(), not inline objects or class name strings. StyleX styles are compiled at build time for optimal deduplication and dead-code elimination.

Simple overrides
tsx
import * as stylex from '@stylexjs/stylex';
​
const overrides = stylex.create({
card: { maxWidth: 400, marginBlock: 16 },
saveButton: { alignSelf: 'flex-end' },
});
​
<Card xstyle={overrides.card} />
<Button label="Save" xstyle={overrides.saveButton} />
Pseudo-classes and conditional styles
tsx
import * as stylex from '@stylexjs/stylex';
​
const overrides = stylex.create({
card: {
boxShadow: {
default: 'none',
':hover': { '@media (hover: hover)': '0 4px 12px rgba(0,0,0,0.1)' },
},
},
});
​
<Card xstyle={overrides.card}>...</Card>
  • All xstyle values must come from stylex.create()
  • Pseudo-classes (:hover, :focus-visible) are supported inside stylex.create
  • All :hover styles MUST use @media (hover: hover) guard
  • For non-StyleX styling (Tailwind, external CSS), use className instead

Tailwind Integration

The package ships a Tailwind v4 theme bridge that maps all design tokens to Tailwind utility classes. Import it once and use Tailwind classes backed by design tokens: colors, spacing, radius, shadows, and typography all resolve to the active theme.

For the imports and the cascade-layer order to put in your global CSS, see astryx docs styling-libraries/tailwind.

Tailwind utilities alongside components
tsx
<div className="text-primary bg-surface rounded-lg p-4 flex gap-3">
<Button label="Save" variant="primary" />
<Button label="Cancel" variant="secondary" />
</div>

The bridge is pure CSS with zero JS. Theme changes (dark mode, custom themes) apply automatically because the utilities reference the same CSS custom properties that components use. This is the paved Tailwind path; for other styling libraries that follow the same aliasing pattern, see astryx docs styling-libraries.

className and style Props

Every component also accepts standard className and style props. className is appended after the component's own classes. style is merged after StyleX inline styles, so consumer values win on conflict.

className with Tailwind utilities
tsx
<Card className="shadow-lg hover:shadow-xl transition-shadow">
...
</Card>
<Button label="Save" className="my-app-save-btn" />

For layout and wrapper styling, Tailwind utilities on className work well. For component-specific overrides (padding, colors, borders), prefer xstyle; it integrates with StyleX deduplication and the component's internal style pipeline.

Rest Props (Prop Drilling)

Components extend HTML attributes and spread rest props onto their root DOM element. This means data-* attributes, aria-* attributes, event handlers, and other HTML props pass through automatically.

Data attributes, event handlers, and ARIA
tsx
<Card
data-testid="user-card"
data-user-id={user.id}
onMouseEnter={handleHover}
aria-label="User profile card"
>
...
</Card>
Ref forwarding
tsx
const cardRef = useRef<HTMLDivElement>(null);
<Card ref={cardRef}>...</Card>

A few HTML attributes are intentionally omitted from the base type (contentEditable, dangerouslySetInnerHTML). children is not in the base type either; components that accept children declare it explicitly, so slot-based components don't silently drop JSX children.

Compound Components

Complex components are composed from smaller components. Each sub-component accepts its own xstyle, className, and rest props. You style the parts individually; there's no single "drill into sub-part" prop.

Dialog with individually styled parts
tsx
import * as stylex from '@stylexjs/stylex';
​
const overrides = stylex.create({
dialog: { maxWidth: 500 },
content: { gap: 'var(--spacing-4)' },
});
​
<Dialog isOpen={isOpen} onOpenChange={close} xstyle={overrides.dialog}>
<Layout
header={
<LayoutHeader hasDivider>
<Heading level={2}>Edit Profile</Heading>
</LayoutHeader>
}
content={
<LayoutContent xstyle={overrides.content}>
<TextInput label="Name" value={name} onChange={setName} />
</LayoutContent>
}
footer={
<LayoutFooter hasDivider>
<Button label="Cancel" variant="secondary" onClick={close} />
<Button label="Save" variant="primary" onClick={save} />
</LayoutFooter>
}
/>
</Dialog>

The pattern: the parent component (Dialog) controls structure and behavior, child components (Layout, Header, Button) control their own appearance. Style each piece where it lives.

Data attribute selectors

When external CSS needs to target an Astryx component by prop or state, combine the stable component class with reflected data attributes. The component class identifies the component (.astryx-button, .astryx-card); data attributes identify the axis and value (data-variant, data-size, data-level, etc.). This is the preferred selector surface for new CSS because it is explicit and collision-resistant.

css
.my-app .astryx-button[data-variant="primary"] {
/* primary buttons in this app context */
}
​
.my-app .astryx-button[data-variant="primary"][data-size="sm"] {
/* small primary buttons */
}
​
.my-app .astryx-heading[data-level="2"] {
/* level 2 headings; numeric values stay literal in data attrs */
}
What components reflect
tsx
// <Button variant="primary" size="sm" />
// preferred selector attrs: data-variant="primary" data-size="sm"
​
// <Card variant="elevated" />
// preferred selector attrs: data-variant="elevated"
​
// <Heading level={2} />
// preferred selector attrs: data-level="2"

For systematic theming, use defineTheme component overrides instead of raw CSS selectors. defineTheme keeps the higher-level prop:value API (variant:primary, size:sm) and handles selector generation for you. Run astryx docs author-a-theme for component theming.

Deprecated: Bare Prop and State Classes

Astryx still emits the deprecated bare classes (.primary, .sm, .level-2, .checked) and will remove them in a later release. Use data attributes for new CSS; astryx upgrade --from <old version> --apply rewrites qualified selectors in .css files.

css
/* The upgrade preserves old consumer classes and adds the v0.6 prop match */
.my-app .astryx-button:is(.primary, [data-variant="primary"]) {
/* primary buttons or a consumer-supplied .primary class */
}
​
/* Numeric values stay literal in data attributes */
.my-app .astryx-heading:is(.level-2, [data-level="2"]) {
/* level 2 headings or a consumer-supplied .level-2 class */
}

The upgrade rewrites a selector only when an .astryx-* component class qualifies it, turning the old class into an :is(...) union of that class and the data attributes it stood for. The union keeps the selector's specificity and your own className matches, and keeps matching once the bare classes are gone; the .astryx-* classes themselves stay. It leaves unqualified classes (a bare .primary), unknown classes, and selectors in JavaScript or TypeScript alone: migrate those by hand, and only where they target Astryx.

What NOT to Do

GuidancePractices
Don't

style={{}} on raw <div> wrappers. Use xstyle on the component directly.

Don't

Hardcoded colors (#fff, rgb(...)). Use var(--color-*) tokens or Tailwind semantic classes (text-primary, bg-surface).

Don't

Hardcoded spacing (16px, 1rem). Use var(--spacing-*) tokens or Tailwind spacing utilities (p-4, gap-3).

Don't

Wrapping a component in a <div> just to add margin. Use xstyle with stylex.create on the component.

Don't

Using !important. If styles aren't applying, check specificity; xstyle is merged last.

Design Tokens

When writing custom styles, use design tokens instead of hardcoded values. Tokens are CSS custom properties that adapt to the active theme and color mode. The system provides tokens for spacing, color, radius, shadow, typography, and size.

Using tokens in stylex.create
tsx
import * as stylex from '@stylexjs/stylex';
​
const styles = stylex.create({
surface: {
padding: 'var(--spacing-4)',
borderRadius: 'var(--radius-container)',
backgroundColor: 'var(--color-background-surface)',
},
});
​
<Card xstyle={styles.surface} />
Using typed token imports in stylex.create
tsx
import {colorVars, spacingVars, radiusVars} from '@astryxdesign/core/theme/tokens.stylex';
​
const styles = stylex.create({
highlight: {
backgroundColor: colorVars['--color-accent-muted'],
padding: spacingVars['--spacing-3'],
borderRadius: radiusVars['--radius-element'],
},
});

Both approaches work: var() strings or typed imports from tokens.stylex. The typed imports give autocomplete and catch typos at build time.

See astryx docs tokens for the full token reference (all spacing, color, radius, shadow, and typography tokens with values). See astryx docs author-a-theme for how to override tokens via defineTheme.

StyleX Build Setup (required for swizzled components)

Astryx components ship pre-compiled, so consuming the published package needs no StyleX setup. But astryx swizzle <Component> copies the raw StyleX *source* into your app, and StyleX source requires a build-time StyleX compiler to produce atomic CSS. Without one the component compiles but renders completely unstyled: no error, no warning. If a swizzled component looks unstyled, a missing StyleX compiler is almost always why. The same applies if you author your own StyleX with stylex.create().

BundlerStyleX plugin
Webpack@stylexjs/webpack-plugin
Vite / Rollup@stylexjs/rollup-plugin (or a community Vite plugin)
Babel (any bundler)@stylexjs/babel-plugin + @stylexjs/postcss-plugin
Next.js (App Router, SWC)An SWC-based transform; see the Next.js note below

Next.js (App Router) is the sharp edge. StyleX's canonical compiler is a Babel plugin, but introducing a Babel config in Next.js disables the SWC compiler, and with it SWC-dependent features like next/font.

The repo's apps/example-nextjs-stylex takes the Babel path (next/babel, @stylexjs/babel-plugin, @stylexjs/postcss-plugin). Babel turns off SWC, so that app does not use next/font. To keep next/font, use an SWC transform such as @stylexswc/nextjs-plugin.

next.config.mjs: SWC-based StyleX transform (keeps next/font working)
js
import stylexPlugin from '@stylexswc/nextjs-plugin';
​
export default stylexPlugin({
rsOptions: {
// Resolve @astryxdesign/core's StyleX so swizzled component source compiles.
aliases: {'@/*': ['./src/*']},
unstable_moduleResolution: {type: 'commonJS'},
},
})({
// your existing Next.js config
});
  • Symptom of a missing compiler: swizzled component renders with no styles, but no build or runtime error.
  • A Babel config turns off SWC in Next.js; skip it if you need next/font.
  • Pure theming (defineTheme + astryx theme build) needs NO StyleX compiler; only swizzled/authored StyleX source does.