Components
Build typed variant APIs with styles.component
styles.component() is the first-class API for variant-driven component styling.
styles.component() is the unified API for all component styling. For flat configs (no dimensioned variants), see Styles.
Use the dimensioned config when you want a typed interface with:
basestylesvariantsdimensionscompoundVariantsfor combinationsdefaultVariants
Basic component
The live example defines a dimensioned button with intent and size variants, then shows how to call it. Class strings follow the global class naming configuration (semantic by default).
Dimensioned variants
Read-only live output. Toggle variants to see the preview, DOM classes, and emitted CSS update together.
Source · button.ts
import { createStyles } from 'typestyles';
const styles = createStyles();
const button = styles.component('variant-button', {
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid transparent',
borderRadius: '8px',
fontWeight: 500,
},
variants: {
intent: {
primary: { backgroundColor: '#2563eb', color: 'white' },
ghost: { backgroundColor: 'transparent', color: '#1f2937' },
},
size: {
sm: { padding: '6px 10px', fontSize: '14px' },
lg: { padding: '10px 16px', fontSize: '16px' },
},
},
defaultVariants: { intent: 'primary', size: 'sm' },
});
Usage
button();
// → "variant-button-base variant-button-intent-primary variant-button-size-sm"
DOM
class="variant-button-base variant-button-intent-primary variant-button-size-sm"
Emitted CSS
.variant-button-base {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 8px;
font-weight: 500;
}
.variant-button-intent-primary {
background-color: #2563eb;
color: white;
}
.variant-button-size-sm {
padding: 6px 10px;
font-size: 14px;
}
Compound variants
Use compoundVariants for styles that should apply only when multiple variant values match.
const badge = styles.component('badge', {
variants: {
tone: {
success: { color: '#166534' },
warning: { color: '#92400e' },
danger: { color: '#991b1b' },
},
size: {
sm: { fontSize: '12px' },
lg: { fontSize: '14px' },
},
},
compoundVariants: [
{
variants: { tone: ['success', 'warning'], size: 'lg' },
style: { fontWeight: 700 },
},
],
});
badge({ tone: 'success', size: 'lg' }); // includes "badge-compound-0"
badge({ tone: 'danger', size: 'lg' }); // does not include compound class
compoundVariants supports:
- single values:
{ size: 'lg' } - multi-value arrays:
{ tone: ['success', 'warning'] }
Boolean variants
Boolean variant dimensions are represented with "true" / "false" option keys.
const input = styles.component('input', {
base: { border: '1px solid #d1d5db' },
variants: {
invalid: {
true: { borderColor: '#ef4444' },
false: { borderColor: '#d1d5db' },
},
},
defaultVariants: {
invalid: false,
},
});
input(); // "input-base input-invalid-false"
input({ invalid: true }); // "input-base input-invalid-true"
Multipart slots
Pass a slots array for components with multiple parts (for example root, trigger, and panel). base, variants, compoundVariants, and defaultVariants can each target specific slot keys.
TypeScript infers each slot name from the array literal, so the return value is typed with those keys (for example tabs.root, tabs.trigger) and unknown keys are errors. You do not need as const on slots when you pass an inline array inside styles.component(...).
const tabs = styles.component('tabs', {
slots: ['root', 'trigger', 'content'],
base: {
root: { display: 'grid' },
trigger: { cursor: 'pointer' },
},
variants: {
size: {
sm: {
trigger: { fontSize: '12px' },
content: { padding: '8px' },
},
lg: {
trigger: { fontSize: '16px' },
content: { padding: '12px' },
},
},
},
defaultVariants: { size: 'sm' },
});
const c = tabs();
c.root; // class string for the root element
c.trigger;
c.content;
Data and ARIA selectors
styles.component supports all CSS selectors:
const accordionTrigger = styles.component('accordion-trigger', {
base: {
'&[data-state="open"]': { fontWeight: 600 },
'&[aria-expanded="true"]': { color: '#1d4ed8' },
},
});
Migration quick-start
From CVA
CVA config maps directly:
cva(base, { variants, compoundVariants, defaultVariants })- to
styles.component(name, { base, variants, compoundVariants, defaultVariants })
The main difference is class generation/injection is handled by typestyles.
See the Migration Guide for library-specific examples.
Expose themeable properties as vars
If you expect a property to vary by theme region, expose it as a component-scoped CSS
custom property instead of hard-coding the value in base or variant styles. Token
and theme overrides then stay on Tier 1
and consumers rarely need plain class overrides or @scope.
const button = styles.component('button', (c) => {
const v = c.vars({
background: { value: '#fff', syntax: '<color>', inherits: false },
foreground: { value: '#111', syntax: '<color>', inherits: false },
});
return {
base: {
backgroundColor: v.background.var,
color: v.foreground.var,
},
variants: {
intent: {
primary: {
[v.background.name]: '#0066ff',
[v.foreground.name]: '#fff',
},
},
},
};
});
The design-system example uses this pattern throughout
(examples/design-system/src/components/button.ts).
Public class name stability
Semantic class names (button-base, button-intent-primary, …) are public API for
consumers theming your package. Do not rename namespaces or variant keys without a
major semver bump. Opt into snapshot + ESLint guardrails described in
Publishing Packages — guard public class names.