Astryx Design System
Astryx (@astryxdesign/core) is this organization's UI toolkit —
components, layout, and styling (via StyleX) all come from it. Never raw HTML
styling, never another component library, never an invented component prop.
You are here because the component you are building pinned this skill, so the whole of it is yours — theming included. The organization's colors are settled in Brand colors below; nobody is interviewed about them, at design time or any other time.
react-webapp owns the app: layout, config, verify sequence, Dockerfile, nginx.
This skill owns what goes inside src/ — the UI. Where the two appear to
disagree, react-webapp wins; the conflicts worth naming are listed under
Platform constraints below.
Correctness through the CLI, not memory
Astryx ships @astryxdesign/cli because component APIs move faster than any
model's training data. The CLI reads the installed version, so it is always
right; a guessed prop is never right by comparison. The discipline: before
writing JSX for a component you have not confirmed this session, run the CLI,
then write the JSX — never the reverse.
Always invoke it as npx --no astryx …. --no restricts resolution to the
@astryxdesign/cli in this app's node_modules — the version package-lock.json
pins. Without it, an npx astryx in an app whose install has not run fetches and
executes the unrelated astryx package that exists on the public registry. Do not
drop the flag to shorten a command: if the CLI is missing, the right outcome is the
loud could not determine executable to run, not a stranger's postinstall script
running in the build pod.
Violating the letter of this rule is violating the spirit of it. "It's just a
placeholder page," "the app doesn't have Astryx wired up yet," and "this screen
is throwaway" are reasons to wire Astryx up faster, not reasons to skip it — a
page built in raw <div>s is what deploys, because there is no human code-review
gate between your PR and the dev environment.
Setup
react-webapp scaffolds the app. Add Astryx to it:
npm install @astryxdesign/core @stylexjs/stylex @astryxdesign/theme-neutral @astryxdesign/build
npm install -D @astryxdesign/cli
@astryxdesign/core declares React 19+ as a hard peer dependency — set
react / react-dom to ^19 in package.json, not an older major.
Wire the build (order matters — astryxStylex() before react(), and no
base, per react-webapp):
// vite.config.ts
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
import {astryxStylex} from '@astryxdesign/build/vite';
export default defineConfig({plugins: [...astryxStylex(), react()]});
// main.tsx — reset + theme CSS load before anything renders
import '@astryxdesign/core/reset.css';
import '@astryxdesign/theme-neutral/theme.css';
import {Theme} from '@astryxdesign/core/theme';
import {neutralTheme} from '@astryxdesign/theme-neutral/built';
// wrap <App/> in <Theme theme={neutralTheme}> — swap the theme package to change
// the look, never hand-roll colors instead
That stock package is the default and the fallback. It is what a project gets when this organization has not set brand colors — see the next section.
Brand colors
A web app is themed at its FIRST build, and retrofitting a theme means revisiting every screen — so the colors cannot be a per-build decision. They are an organization decision, settled once in the section below and compiled at build time, exactly like the language a service is written in.
Never ask anyone for them. Not at design time, not at coding time, not once per project. The answer is either set below or it is the stock theme, and both are complete answers — a question about theming is a defect, not diligence.
The organization's colors
- Accent (buttons, links, focus): not set — use the stock theme
- Neutral (backgrounds, surfaces): not set — use the stock theme
To brand every web app this organization builds, an org edits those two lines to hex values (Settings → Skills), e.g.:
- Accent (buttons, links, focus): #f5c518
- Neutral (backgrounds, surfaces): #0a0a0a
HEX only, never color words: "black and yellow" does not say WHICH yellow, and this file is the whole of what a build sees. The edit reaches every subsequent build in the org with no conversation involved.
A per-project override still wins, per value, not per section: a hex under
the ## Brand colors heading in the project's specs/requirements/prd.md
overrides the same line here, and a line that heading omits still comes from
this section. That heading is there for a project whose colors someone stated
outright — it is not something to solicit, and a half-filled one is not a
reason to drop the organization's other color.
At build time — compile the theme
Resolve each of the two colors before you wire the theme, independently: the
project's ## Brand colors line in specs/requirements/prd.md if it has one,
otherwise the line in The organization's colors above. A color neither one
sets is not chosen — take it from the stock theme's own token and never
invent one, whether that leaves you with two brand colors, one, or none. One
brand color plus one stock color is a valid outcome; a guessed hex is not.
With colors, a brand theme is a compiled theme of your own — not hand-written colors sprinkled over a stock one. Editing component styles to paint them brand colors violates "colors are tokens" and leaves every unstyled surface off-brand. Scaffold a stock theme as editable source, retune its color tokens, compile it:
# 1. scaffold the closest stock theme as YOUR source (once)
npx --no astryx theme add neutral src/theme
# 2. copy it to src/theme/brandTheme.ts, rename the exported symbol +
# `name:` + its defineSyntaxTheme `name:`, then edit the color tokens
# 3. compile — emits brand.css + brand.js + brand.d.ts beside the source
npx --no astryx theme build src/theme/brandTheme.ts -o src/theme/brand.css
Every color token is a [light, dark] pair. Retune this set and leave the rest
of the scaffold alone — they are what carry a brand:
| Token | Set it to |
|---|---|
--color-accent, --color-text-accent, --color-icon-accent |
the accent brand color; the light slot usually needs a darkened variant to stay AA on a light background |
--color-on-accent |
the text color that sits ON the accent — check contrast both ways |
--color-accent-muted |
the accent at low alpha (e.g. #RRGGBB33) |
--color-background-body, --color-background-surface, --color-background-card, --color-background-popover, --color-background-muted |
the neutral brand color, as a ramp — body darkest, surface/card a step lighter, never all the same value |
Then wire the compiled output exactly like a package theme — the import is the
build's -o CSS plus the sibling JS, both from src/theme/, never the .ts:
// main.tsx
import '@astryxdesign/core/reset.css';
import './theme/brand.css';
import {Theme} from '@astryxdesign/core/theme';
import {brandTheme} from './theme/brand';
// wrap <App/> in <Theme theme={brandTheme}>
Commit the generated brand.css / brand.js / brand.d.ts alongside the
source: the Docker build runs npm run build, not the theme compiler, so an
uncommitted build output ships an unthemed app. Re-run theme build after
every edit to brandTheme.ts — editing the source alone changes nothing.
Two colors is the common case, and it does not mean two tokens. Map the neutral
across the background ramp and the accent across the accent trio; a pure
#FFFF00-class hue almost always needs darkening for its light-mode slot and
dark text for --color-on-accent. Contrast is not negotiable to match a brand:
keep the hue, move the lightness.
Verify
This skill's step in react-webapp's verify sequence — after npm install,
before npx tsc --noEmit:
npx --no astryx doctor
A non-zero exit fails verification like any other step in that sequence. It is
there because a wiring fault (a missing astryxStylex() plugin, an unimported
theme, a React peer-dependency mismatch) type-checks and builds perfectly clean,
then renders an unstyled page in the cluster — cheap to fix here, expensive to
debug after the Docker build.
Platform constraints that override this system's defaults
Four places where Astryx's own defaults do not fit this platform. Each is a runtime, build, or guidance failure, not a style preference:
- Never install Astryx's agent docs.
astryx inititself is fine — it initializes the design system in an existing project, and it is not required, because the Setup section above already states the wiring. What must not land is theagentsfeature:astryx init --features agentswritesAGENTS.mdinto the repo root, and--allincludes it. Guidance reaches you as skills, so a committed agent file is a second authority that nothing updates — it is stale the moment this skill changes.--featuresis an allow-list, so name only what you want (--features theme) and never--all; if agent docs already landed,astryx init --remove-agentsdeletes them. - Never set
baseinvite.config.ts, whatever an Astryx snippet shows. Each web app is served at its own gateway host root; abase404s every asset and the page renders blank (react-webapp, Served at host root). - The CSS imports go in
main.tsx, never inindex.html.index.html's only<script>rules arereact-webapp's: the synchronousenv-config.jstag first, the module bundle second. Adding a stylesheet or script tag around them riskswindow._env_being unset when the first module evaluates. - Theme tokens are not runtime config. Colors and spacing come from the
theme package at build time.
window._env_carries only what the browser needs — OIDC config and flags — so do not plumb a theme value through it.
Astryx replaces hand-written UI, not the platform's data layer: openapi-fetch
and the committed src/generated/ client stay exactly as react-webapp
specifies. "Install no other library" below is about UI and styling.
Critical rules
- Import everything from
@astryxdesign/core/<Category>(per-category subpath entry points, e.g.@astryxdesign/core/Button,@astryxdesign/core/Layout) — never from Tailwind, MUI, Chakra, Ant Design, Bootstrap, or a hand-rolled component. - Run
npx --no astryx component <Name> --densebefore using ANY component, even one already used earlier in this session — confirm the prop exists before writing it, don't guess. - Search before building. Run
npx --no astryx search "<thing>"when unsure what exists; Astryx ships more components than you would assume (tag inputs, command palettes, tree lists, chat UI) — check before reaching for a wrapper<div>or a new dependency. - Layout is
VStack/HStack/Grid/Stackfrom@astryxdesign/core/Layout— never a raw<div>/<span>for spacing or arrangement. - Style overrides are
stylex.create()+ the component'sxstyleprop — neverstyle={{...}}, and neverclassName/stylealongside{...stylex.props()}(usemergeProps()if you must combine). - Colors and spacing are tokens, never literals. Run
npx --no astryx docs tokens --dense; use the CSS-var color tokens andspaceNgap values it documents, not hex/rgb or raw px. - Page-level structure follows a template, not intuition. Run
npx --no astryx template --listandnpx --no astryx template <name> --skeletonto find and study a layout skeleton before hand-building a page (dashboard, settings, list, wizard, auth) from scratch. - Navigation uses
useLinkComponent(), never a hardcoded<a>. - Dense data is rows, not cards. Use
TableorList+Itemfor lists of records;Cardis for widgets, galleries, or grouped settings — not one card per row.
Reach for these components (not raw HTML)
| If you're about to build… | Use instead |
|---|---|
| Page shell with top bar + side nav | AppShell, TopNav, SideNav, MobileNav |
| A data table / list view | Table, List + Item, MetadataList |
| A form with grouped fields | FormLayout, Field, FieldStatus |
| A select / combobox / tag input | Selector, MultiSelector, ComplexSelector, Typeahead, Tokenizer |
| A modal / confirmation dialog | Dialog, AlertDialog |
| A dropdown / context / command menu | DropdownMenu, ContextMenu, MoreMenu, CommandPalette |
| Status / count / label chip | Badge, StatusDot, Token, Indicator |
| Tooltip / hover detail / anchored popup | Tooltip, HoverCard, Popover |
| Date/time entry | DateInput, DateRangeInput, DateTimeInput, TimeInput, Calendar |
| Loading / empty state | Skeleton, Spinner, ProgressBar, EmptyState |
| Breadcrumbs / global search | Breadcrumbs, PowerSearch |
| Toggle / choice input | Switch, CheckboxInput, CheckboxList, RadioList, SegmentedControl, ToggleButton |
This table is a quick guide, not the catalog — run
npx --no astryx component --list for every component grouped by category, or
npx --no astryx search when nothing here fits.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Astryx components render unstyled/unthemed | reset.css/theme CSS not imported, or imported after other global CSS without layer ordering |
Import reset + theme first in main.tsx; if the app has other global CSS, assign every stylesheet an explicit @layer (npx --no astryx docs migration) |
npm install fails / peer-dependency warnings on React |
package.json is on React <19 |
Set react/react-dom to ^19 before installing @astryxdesign/core |
| Build succeeds but StyleX classes/styles don't apply | astryxStylex() missing from vite.config.ts, or ordered after react() |
Add ...astryxStylex() to plugins, listed before react() |
| Page renders blank in dev, every asset 404s | base was set in vite.config.ts from an Astryx snippet |
Remove it — served at host root (react-webapp) |
| A prop doesn't exist, or is the old spelling | Answered from memory instead of the CLI | Run npx --no astryx component <Name> --dense — the CLI reflects the installed version, training data doesn't |
Every row in a list is wrapped in its own Card |
Defaulted to a generic "card grid" instead of checking data density | npx --no astryx docs principles --dense — dense data is Table/List+Item; Card is for widgets/galleries/settings groups |
| Brand colors are set, deployed app is stock-themed | Colors were read but never compiled into a theme, or brandTheme.ts was edited without re-running theme build |
Re-run npx --no astryx theme build src/theme/brandTheme.ts -o src/theme/brand.css and confirm brand.css/brand.js are COMMITTED — the image build never runs the compiler |
| The user gave brand colors in chat, the build ignored them | A coding run never sees a conversation — colors reach it only from this skill or the project's specs/requirements/prd.md |
Set them in The organization's colors (Settings → Skills) for the whole org, or under ## Brand colors in the project's specs/requirements/prd.md for one project; an answer that is not in a file did not happen |
| Brand accent is unreadable — pale text on a pale button | The brand hex was pasted into both [light, dark] slots of --color-accent |
Darken the light-mode slot and set --color-on-accent to a color that contrasts with the accent in each mode |
Red flags — stop and use Astryx
- About to write
<div style={{...}}>or a rawclassNamefor layout, color, or spacing - About to
npm installany other component or styling library - About to write JSX for a form, list, card, dialog, nav, or button from scratch
- Thinking "it's just a placeholder" or "Astryx isn't set up in this app yet"
- Using a prop without having confirmed it exists via
astryx component <Name> --dense - About to satisfy a brand-color requirement by styling components instead of compiling a theme — or about to ignore one because no stock theme matches
- About to ask which theme or colors to use — that is settled in Brand colors, and "not set" means the stock theme, not an open question
All of these mean: stop, run astryx search / astryx component <Name> --dense,
and use what it returns.