Styling ComponentsHow to customize component appearance: xstyle prop, Tailwind, StyleX, className, rest props, compound component patterns, and theming hooks.
OverviewXDS gives you several ways to style things. Here is when to use each:
ApproachUse forExample
xstyle propOverriding a specific XDS componentxstyle={styles.override}
Tailwind utilitiesLayout, wrappers, and utility stylingclassName="flex gap-3 p-4"
stylex.createReusable styles, pseudo-classes, typed tokensstylex.create({ card: { ... } })
classNameIntegrating with external CSS or Tailwind on componentsclassName="my-card shadow-lg"
All approaches resolve to the same XDS design tokens, so theming and dark mode work regardless of which you choose.
xstyle PropEvery XDS component accepts an xstyle prop for style customization. It accepts StyleX styles created via stylex.create() — not inline objects, not 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' },
});
<XDSCard xstyle={overrides.card} />
<XDSButton 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)' },
},
},
});
<XDSCard xstyle={overrides.card}>...</XDSCard>
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 IntegrationXDS ships a Tailwind v4 theme bridge that maps all design tokens to Tailwind utility classes. Import it once and use Tailwind classes backed by XDS tokens — colors, spacing, radius, shadows, and typography all resolve to the active theme.
globals.css — import the bridge
css
@import "tailwindcss";
@import "@xds/core/tailwind-theme.css" layer(theme);
Tailwind utilities alongside XDS components
tsx
<div className="text-primary bg-surface rounded-container p-4 flex gap-3">
<XDSButton label="Save" variant="primary" />
<XDSButton 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 XDS components use.
className and style PropsEvery 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
<XDSCard className="shadow-lg hover:shadow-xl transition-shadow">
...
</XDSCard>
<XDSButton 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)XDS 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
<XDSCard
data-testid="user-card"
data-user-id={user.id}
onMouseEnter={handleHover}
aria-label="User profile card"
>
...
</XDSCard>
Ref forwarding
tsx
const cardRef = useRef<HTMLDivElement>(null);
<XDSCard ref={cardRef}>...</XDSCard>
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 ComponentsComplex components are composed from smaller XDS 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)' },
});
<XDSDialog isOpen={isOpen} onClose={close} xstyle={overrides.dialog}>
<XDSLayout
header={
<XDSLayoutHeader hasDivider>
<XDSHeading level={2}>Edit Profile</XDSHeading>
</XDSLayoutHeader>
}
content={
<XDSLayoutContent xstyle={overrides.content}>
<XDSTextInput label="Name" value={name} onChange={setName} />
</XDSLayoutContent>
}
footer={
<XDSLayoutFooter hasDivider>
<XDSButton label="Cancel" variant="secondary" onClick={close} />
<XDSButton label="Save" variant="primary" onClick={save} />
</XDSLayoutFooter>
}
/>
</XDSDialog>
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.
Theming via xds-* Class NamesEvery component renders a stable xds-* class name (e.g. xds-button, xds-card) plus variant classes. These are the targeting surface for theme overrides in defineTheme. You rarely need to use them directly, but they're useful for debugging and for external CSS that needs to target XDS components.
Class names rendered by components
tsx
// <XDSButton variant="primary" size="sm" />
// renders: class="xds-button primary sm ..."
// <XDSCard variant="elevated" />
// renders: class="xds-card elevated ..."
// <XDSHeading level={2} />
// renders: class="xds-heading level-2 ..."
Targeting in external CSS (escape hatch)
css
.my-app .xds-button.primary {
/* override primary button in this app context */
}
For systematic theming, use defineTheme component overrides instead of raw CSS selectors. Run `npx xds docs theme` for the full theming guide.
Design TokensWhen writing custom styles, use design tokens instead of hardcoded values. Tokens are CSS custom properties that adapt to the active theme and color mode. XDS 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)',
},
});
<XDSCard xstyle={styles.surface} />
Using typed token imports in stylex.create
tsx
import {colorVars, spacingVars, radiusVars} from '@xds/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 `npx xds docs tokens` for the full token reference (all spacing, color, radius, shadow, and typography tokens with values). See `npx xds docs theme` for how to override tokens via defineTheme.
What NOT to Do
GuidancePractices
Don'tstyle={{}} on raw <div> wrappers. Use xstyle on the XDS component directly.
Don'tHardcoded colors (#fff, rgb(...)). Use var(--color-*) tokens or Tailwind semantic classes (text-primary, bg-surface).
Don'tHardcoded spacing (16px, 1rem). Use var(--spacing-*) tokens or Tailwind spacing utilities (p-4, gap-3).
Don'tWrapping an XDS component in a <div> just to add margin. Use xstyle with stylex.create on the component.
Don'tUsing !important. If styles aren't applying, check specificity — xstyle is merged last.