Installation
Get StyleX compiling, then paste the token layer every component reads.
You install carbon by copying component source into your project. Two things have to exist there before any component works: the StyleX compiler and the token layer the components read.
Both fail quietly. A missing compiler leaves stylex.props() returning class
names with no stylesheet behind them. A misconfigured one resolves a token to
an empty variable and the declaration drops.
Setup
These steps assume a Vite + React + TypeScript project. Every file path below
is relative to src/. Delete the starter's src/index.css first: it sets
--border and color-scheme on :root outside any layer, and that beats
carbon's values.
Install the runtime, Base UI, the fonts and the compiler.
Add the plugin to your Vite config. It goes first in the array, before the React plugin, so it sees the source before anything else rewrites it.
Each option is required:
aliaseslets the compiler resolve@/theme/tokens.stylex. Without it the build fails with "Could not resolve the path to the imported file".useCSSLayersputs the reset layer thattheme.cssopens under every atomic class. Without it the reset's* { margin: 0 }beats every StyleX declaration.resolve.tsconfigPathslets Vite resolve the same@/imports in dev.build.cssTargetstops Vite's CSS minifier from rewritinglight-dark(), which every themed token uses.
The plugin appends the compiled stylesheet to your CSS bundle. You do not need
an @import or a virtual module.
Declare your browser floor. The compiler runs its CSS through lightningcss with
your browserslist as the target, and the default targets are old enough that
light-dark() gets downleveled into a polyfill that only works in production
bundles. Baseline 2024 keeps it native, and matches cssTarget above.
Point @/* at src/. In a Vite project the compiler options live in
tsconfig.app.json; the root tsconfig.json only lists references.
In tsconfig.node.json, which type-checks vite.config.ts, set
"module": "esnext" and "moduleResolution": "bundler". Under the template's
nodenext, TypeScript reads the StyleX plugin's default export as a namespace
and tsc -b fails with "This expression is not callable".
Copy theme.css. It holds the element reset, the color-scheme switch that
drives dark mode, and the font stacks.
Copy the token module. Values two components share live here: themed colors
are defineVars with light-dark() pairs, fixed scales are defineConsts
(space, radius, text, leading, tracking, shadow, ease,
duration, layer), and the interop group publishes the flat custom
properties the reset reads. To retheme, edit this file or scope a
stylex.createTheme(color, {…}) over a subtree.
Copy the breakpoints. StyleX puts a media query inside a property's value rather than around the rule, so breakpoints are constants you import.
Copy the markers. A part that reacts to its ancestor's state, such as an
accordion's chevron when the trigger is hovered or a sidebar row's action when
the row is, reads that state through a marker. StyleX only compiles markers
inside a .stylex.ts file.
Copy the shared state styles. Every control shows the same focus ring, the same destructive ring when it is invalid and the same dim when it is disabled, and every popup sits on one of two surfaces, so those are written here and merged into a component's own styles.
Copy the state-props helper. A Base UI part hands its state to a className
callback, and stylex.props() returns a style as well as a class. The helper
returns both callbacks from one list of styles, so a dynamic style keeps its
inline variable.
Copy interop.css. It holds the rules StyleX cannot author: @property
registrations, the scroll-driven edge masks, icon sizing inside components, and
drawer gesture rules.
Import both stylesheets at the top of your entry file, theme.css first.
Render a component to check the setup. A failed token resolution drops the declaration without failing the build, so a successful build proves nothing.
The first vite start after installing can show an "Invalid empty selector"
overlay from the StyleX plugin. Reload the page once. The production build is
not affected.
Dark mode
color-scheme controls dark mode. Every themed token is a light-dark() pair, and
theme.css sets color-scheme: light dark on :root, so the page follows the
OS with no JS. Anything that differs between light and dark is a light-dark()
pair, written once, in tokens.stylex.ts or in the one component that uses it.
A theme toggle only has to force the scheme. .light and .dark in
theme.css pin color-scheme, and every token follows. Set the class before
first paint or a forced theme flashes:
The storage key is yours to choose, but it must match the one your theme switcher writes. A mismatch means the pre-paint script and the toggle disagree until the first click.
light-dark() only works on colors. A non-color value that differs by theme,
such as a transform or a font-style, needs two rules: a .dark override for
the forced scheme, plus a @media (prefers-color-scheme: dark) { :root:not(.light) … }
twin for system mode.
Conventions
Every component takes overrides the same way:
xstyle takes StyleX styles, and the component merges it last, so it wins.
style is still real inline CSS, for values only the runtime knows. className
is a hook for plain CSS. It is concatenated and nothing resolves conflicts, so
anything that must beat a component's own declaration goes through xstyle.
Field state
Field publishes its resolved state for plain layout parts through field-state.ts.