Adopting Astryx in an existing Next.js + Tailwind app
Integration guide for adding Astryx to an existing Next.js + Tailwind project: the cascade-layer trap, moduleResolution, StyleX build, and custom variants.Overview
Adding Astryx to a fresh app is smooth. Adding it to an *existing* Next.js + Tailwind project hits a few integration-boundary traps that fail silently — no errors, just styles that appear ignored. This guide collects them so you do not have to rediscover them. Each is individually small; together they are most of the first-time friction.
The theming model itself (defineTheme + astryx theme build, or the <Theme> runtime) needs no special build setup. The friction is almost entirely at the integration boundary: CSS cascade layers, TypeScript module resolution, and — only if you swizzle — the StyleX build.
Trap 1: Tailwind preflight silently defeats theme overrides
This is the highest-impact trap and it produces ZERO errors — the theme just appears ignored (buttons transparent, h1 rendering at ~16px, etc.). Astryx emits component theme overrides in the astryx-theme cascade layer. Tailwind’s preflight (@tailwind base) is emitted UNLAYERED. Per the CSS cascade, unlayered rules beat any @layer rule regardless of specificity — so every Astryx theme override loses to preflight, silently.
The fix is to put Tailwind’s layers into named cascade layers and declare the full layer order up front, so Astryx’s layers resolve in the right place. Unlayered consumer CSS still wins over everything, so your existing app is unaffected.
css/* Declare the full layer order up front (lowest -> highest priority). */@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;@import "tailwindcss/theme.css" layer(theme);@import "tailwindcss/preflight.css" layer(base);@import "@astryxdesign/core/reset.css";@import "@astryxdesign/core/astryx.css";@import "@astryxdesign/theme-neutral/theme.css";@import "@astryxdesign/core/tailwind-theme.css";@import "tailwindcss/utilities.css" layer(utilities);
css/* Tailwind v3 emits @tailwind base UNLAYERED by default, which beats every* @layer rule. Wrap it in a named layer so astryx-theme can win. */@layer tw-preflight {@tailwind base;}@tailwind components;@tailwind utilities;@import "@astryxdesign/core/reset.css";@import "@astryxdesign/core/astryx.css";@import "@astryxdesign/theme-neutral/theme.css";
See the working reference app apps/example-nextjs-tailwind in the repo for a complete Tailwind v4 layer setup.
Trap 2: moduleResolution must be "bundler" (or node16+)
Astryx ships subpath exports (@astryxdesign/core/Button, /theme, /Link, ...). With the very common "moduleResolution": "node" in tsconfig, these subpaths do not resolve and imports fail to type-check. Switch to "bundler" (or "node16"/"nodenext").
json{"compilerOptions": {"moduleResolution": "bundler"}}
Heads up: this is a project-wide change. Switching resolution can surface latent issues in unrelated dependencies (e.g. a package whose exports map does not append extensions may now need explicit .js in its imports). Expect to fix a few unrelated imports when you flip it, rather than being surprised later.
Trap 3: swizzled components need a StyleX compiler
Consuming the published Astryx package needs no StyleX setup — components ship pre-compiled. But astryx swizzle <Component> copies raw StyleX source into your app, which requires a build-time StyleX compiler or it renders unstyled with no error. On Next.js App Router specifically, the StyleX Babel plugin disables SWC and breaks next/font, so an SWC-based transform is required.
Full setup (per-bundler, plus the Next.js/SWC path) is in npx astryx docs styling under "StyleX Build Setup". The working reference is apps/example-nextjs-stylex in the repo.
Trap 4: custom variants are type-safe (via defineTheme + theme build)
You can add a custom component variant in your theme (e.g. components.button["variant:accentOutline"]) without swizzling. astryx theme build emits both the CSS and a <name>.variants.d.ts type augmentation so variant="accentOutline" type-checks. The generated theme .d.ts references the augmentation file, so importing your built theme loads it automatically.
tsimport { defineTheme } from "@astryxdesign/core/theme";export const myTheme = defineTheme({name: "mytheme",components: {button: {"variant:accentOutline": {backgroundColor: "transparent",color: "var(--color-accent)",borderColor: "var(--color-accent)",},},},});
Note: only props backed by an extensible interface (like Button variant) are augmentable. Closed sizes/types (Button size, Heading type) are not theme-extensible — use a swizzle for those. See npx astryx docs theme.
Discoverability: start with the CLI
Most "how do I theme X?" time is spent not knowing the answer already exists in the CLI. Before assuming a limitation, check:
astryx component <Name>— themeable slots, override keys, and CSS vars for a component.astryx docs tokens— the full design-token namespace.astryx docs styling— every styling approach + the StyleX build setup.astryx docs theme— how to override tokens and add custom variants via defineTheme.
Net
The theming model is a pleasure once it works — the friction is at the integration boundary. Layer your Tailwind preflight (Trap 1), set moduleResolution (Trap 2), and only worry about StyleX if you swizzle (Trap 3). After that, drive the whole surface from a theme.