Noctis

Using StyleX

Noctis components are precompiled CSS keyed off data-slot, not a styling framework — so a StyleX app consumes them the same way any app does, with nothing to add to the StyleX compiler's include and no package to transpile. What you do need is the cascade pinned so your styles win, and the canonical custom properties read as literal values so every token still re-resolves inside an elevation scope.

Setup

Install the package and import the stylesheet once, at the root:

TSX
import "@stridge/noctis/styles.css";

That is the whole Noctis side. There is no StyleX adapter to install, no subpath to import, and nothing from node_modules for the compiler to scan — component styling arrives as plain CSS, already compiled.

Configure StyleX exactly as its own docs describe. The only Noctis-specific decision is where its output lands in the cascade.

Pin the cascade

This is the one setting that matters, and getting it wrong is silent — your styles compile, they just lose.

Noctis paints inside @layer noctis.components, and its rules carry a data-* recipe:

CSS
[data-button][data-variant="primary"] { background-color: var(--noctis-color-primary); }

That selector has specificity (0,2,0). Every StyleX atomic class is (0,1,0), so specificity alone always loses. What decides the contest is the cascade layer, and there are two working configurations.

Leave StyleX unlayered. This is its default. Unlayered CSS beats every layer regardless of specificity, so an app style wins outright with no further configuration. Declare the Noctis order in your entry stylesheet and put StyleX's output after it:

CSS
@layer noctis.engine, noctis.components;
@import "@stridge/noctis/styles.css";
@stylex; /* the PostCSS plugin substitutes the generated CSS here */

Or name the Noctis layers first. If you enable StyleX's layers, list them in before so StyleX's priority layers are declared after Noctis's:

JavaScript
useLayers: { before: ["noctis.engine", "noctis.components"] }

which emits the order header you want:

CSS
@layer noctis.engine, noctis.components, priority1, priority2, priority3, priority4;

Reading tokens

Write the canonical custom property as a literal string. StyleX accepts it as a value and compiles it through untouched:

TSX
import * as stylex from "@stylexjs/stylex";

const styles = stylex.create({
    card: {
        backgroundColor: "var(--noctis-color-surface)",
        borderColor: "var(--noctis-color-border)",
        borderRadius: "var(--noctis-radius-md)",
        padding: "var(--noctis-space-4)",
        color: "var(--noctis-color-foreground)",
    },
});

This is not a workaround — it is exactly what the precompiled component CSS does, and it is what keeps the theming model intact. Because the property is read at the element the class lands on, a value re-resolves against whatever scope encloses it: the same card above paints the root surface on the page and the menu surface inside a data-elevation="menu" popup, with no second style and no JavaScript.

The full role catalog is on the tokens page; the private --noctis-engine-* ramp is not part of it.

Media queries

Breakpoints are the one family that cannot be read as a custom property — a media query prelude rejects var(). Write the query literally, or hold the strings in stylex.defineConsts, which is exactly what it is for:

TSX
const breakpoints = stylex.defineConsts({
    md: "@media (min-width: 48rem)",
});

const styles = stylex.create({
    panel: {
        padding: { default: "var(--noctis-space-4)", [breakpoints.md]: "var(--noctis-space-8)" },
    },
});
BreakpointMin width
sm40rem
md48rem
lg64rem
xl80rem
2xl96rem

Why not StyleX variables

stylex.defineVars looks like the natural home for a design system's tokens, and it is the wrong tool here. It declares its own hashed property at the root:

CSS
:root, .x6mqh16 { --x1bdcq69: var(--noctis-color-surface); }

var() is substituted at computed-value time on the element that declares the property, and descendants inherit the substituted value. So the alias is frozen to whatever the root resolved to — and re-declaring --noctis-color-surface inside a subtree never reaches it. Every elevation scope, every ThemeScope, and every ColorScope would silently paint the root colour. Nothing errors; the colours are just wrong.

Wrapping the tokens in stylex.defineConsts avoids that — constants are inlined at each use site rather than aliased — but an imported constant is not inlined across a module boundary today, so it cannot be published from a package.

Reading the canonical properties directly sidesteps both. It is the supported path, and it is the one that preserves per-element resolution.

Overriding a component

Every component and subpart forwards both className and style, so spreading stylex.props onto one works with no wrapper:

TSX
<Button variant="ghost" {...stylex.props(styles.wide)} />
<Select.Item {...stylex.props(styles.dense)} />

For anything beyond geometry, prefer a rung of the customization ladder over a local override — it keeps the change in the system rather than at one call site. A component token is set like any other custom property:

TSX
const styles = stylex.create({
    tallButtons: { "--noctis-button-height": "2.75rem" },
});

What StyleX cannot express

StyleX allows pseudo-classes and pseudo-elements on the element itself, and forbids descendant, child, and sibling combinators. Two Noctis idioms fall outside it:

  • Self-attribute conditions. The popup enter/exit transition keys off [data-starting-style] and [data-ending-style], which Base UI stamps at runtime. stylex.when observes ancestors, descendants, and siblings — not the element's own attributes — so this has no StyleX form.
  • Reaching into a component's parts. Styling a descendant slot from an ancestor is a combinator by nature.

Both are already solved without StyleX: components accept an elevation/shade recipe through Surface.props() and friends, and a [data-slot="noctis-…"] rule in a plain stylesheet is the documented bottom rung of the ladder. Reach for a stylesheet for these, and keep StyleX for the layout and glue it is good at.