Adoption feedback: migrating a real Next.js 16 app to Astryx (0.1.8) — 10 findings + suggestions
- Dominant language
- TypeScript
- Stars
- 13.1k
- Forks
- 1.1k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
## Adoption feedback: migrating a real Next.js 16 app to Astryx (0.1.8)
We migrated a production dark-only chat/transcript viewer onto `@astryxdesign/core@0.1.8` — Next.js 16 App Router, React 19, **webpack** build, pnpm 10 (strict `node_modules`), a strict CSP (`style-src 'self' 'unsafe-inline'`, `font-src 'self'`, `connect-src 'self'`), and ~130 Playwright locators against our own CSS classes. Overall it went well, and a few things were genuinely excellent — so first, credit where it's due, then the actionable findings.
Happy to split any of these into individual tracked issues if you'd prefer; consolidating so triage is one read.
### What worked really well
- **`BaseProps` forwards `className` / `style` / `data-*` / `aria-*`.** This is what made an *incremental* migration of an app with ~130 CSS-selector tests possible at all — we could swap components without rewriting the test suite in the same commit. Arguably the single most important adoption property; worth advertising louder.
- **The CLI is a better docs surface than the website.** `astryx docs ` / `component ` / `hook` / `template`, plus `--json` and `--dense`, gave complete, offline, machine-readable docs. We initially tried scraping the docs site (Next RSC payload, fetches poorly) and threw it away once we found the CLI.
- **`astryx docs migration` predicted our worst bug before we hit it** — the "Cascade Layer Safety" section names the exact webpack `@import`-hoisting failure and the `layers.css` remedy. Docs that name the specific silent failure of a specific bundler are rare.
- **Pre-compiled CSS is genuinely build-step-free on Next+webpack**, and CSP-clean (no `@font-face`, no remote `url()` in `reset.css`/`astryx.css`/the built theme).
- **`TextArea`'s iOS auto-zoom guard** (`@media (pointer: coarse) { font-size: max(1rem, …) }`) is pointer-aware — better than the unconditional 16px we had.
---
### Bugs / friction (roughly by cost)
**1. `@stylexjs/stylex` is missing from the documented install line — hard runtime failure on pnpm.**
README Quick Start and `astryx docs getting-started` both say `npm install @astryxdesign/core @astryxdesign/theme-neutral`. But `@stylexjs/stylex` is a *peer dependency* of core, and **202 of `dist/`'s files import it at runtime**:
```
$ grep -rl "@stylexjs/stylex" node_modules/@astryxdesign/core/dist --include="*.js" | wc -l
202
```
npm/yarn auto-install peers, so it's invisible there. **pnpm does not** — under strict `node_modules` every component throws on import. Suggest adding it to the documented install line, or moving it to `dependencies` (consumers who don't author StyleX have no reason to pin its version). An `astryx doctor` check would also catch it.
**2. `astryx theme build`'s default output location is a trap for a `.ts` theme source.**
The default writes `.{css,js,d.ts,variants.d.ts}` *next to* the source, so `remote-claw.js` lands beside `remote-claw.ts`. With `next.config`'s `experimental.extensionAlias: { ".js": [".ts", …] }` (which many TS monorepos set), `import "./theme/remote-claw.js"` then silently resolves to the **source** theme (runtime `` injection) instead of the built one — same export name, same shape, just without `__built: true`. `--out` fixes it and relocates the *whole* set (all four artifacts follow the CSS path's dirname), which we only discovered by reading the CLI source. `astryx theme build --help` describes `-o, --out` as "Output CSS file path", which reads as CSS-only. Suggest: "output directory for all theme artifacts (named by the CSS path)", plus a note in `astryx docs theme` that the default co-locates a `.js` beside a `.ts` source.
**3. Generated artifacts carry a `Generated: <ISO timestamp>` header, so committed output can't be drift-checked.**
We commit the built theme so CI/Vercel never run the CLI. The timestamp makes *every* rebuild a diff, defeating a "did someone edit the built file instead of the source?" gate; we post-process the line away for byte-identical rebuilds. `Source:`/`Command:` already carry the useful provenance — suggest dropping the timestamp or gating it behind a flag (`SOURCE_DATE_EPOCH` / `--no-timestamp`). Reproducible generated output is worth a lot.
**4. The `astryx init` nudge prints on **stdout** of every command.**
`astryx docs theme | head -2` begins with `Next step: run 'pnpm exec astryx init' …`. It's prepended to `docs`/`component`/`hook`/`template` stdout, so anything that pipes/captures has to strip it. `--json` is clean (right instinct) — suggest sending the nudge to **stderr** so it stays visible to a human but disappears from pipes.
**5. Minor: `astryx docs --list` errors** with `unknown option '--list'`, even though bare `astryx docs` prints exactly that list and `component --list` / `template --list` both exist.
---
### Theming / behavior
**6. `defineTheme`'s accent scale **inverts** the accent in dark mode — with no warning at the place you'd look.**
Seeding the family the documented way, `color: { accent: '#5457e8' }`, generates:
```
--color-accent: light-dark(#424BDA, #CBBEFF)
--color-on-accent: light-dark(#FFFFFF, #001F9C)
```
i.e. in dark mode the accent becomes a **pale surface carrying dark text**. Coherent as a system default, but a surprise when the reason you're writing a theme is to carry a fixed brand across — our primary button silently became pale lavender with dark-blue text (measured, not eyeballed; our full test suite stayed green because our design guard asserted the disabled *treatment*, not the hue). The fix is supported — pin **both** halves — but `astryx docs theme` only warns *against* hand-writing `--color-accent` alone, which nudges you toward the scale API and away from the thing you actually need. Suggest documenting that the seed inverts for dark mode, with the pin-both-halves snippet as the supported way to keep a fixed brand fill.
**7. Focus rings come from `--color-accent`, which fights a pinned brand fill — and there's no focus token.**
`astryx.css` draws focus as `outline: 2px solid var(--color-accent)`. A theme whose accent is a dark, saturated *fill* (correct for white-on-accent buttons) therefore gets a dark, low-contrast ring on a dark background (~3.4:1 vs the 5.8:1 our text-accent would give). It still clears WCAG 2.2 SC 1.4.11's 3:1, so nothing is broken — just dimmer than intended, and invisible until you tab through on a dark theme. Suggest a dedicated `--color-focus-ring` defaulting to `--color-accent`, so a dark theme with a saturated brand can set fill and ring independently.
**8. `<Text size>` is silently inert whenever the theme styles that `type`.**
`size` is documented as an override, but on a themed app it does nothing for any `type` the theme emits a rule for:
```tsx
<Text type="supporting" size="xsm">…</Text>
// class="astryx-text supporting xsm …", computed font-size: 12px ← size had no effect
```
It's cascade layers, not specificity: `@layer astryx-theme { .astryx-text.supporting { font-size: … } }` beats the `xsm` size class in `@layer astryx-base` every time, and the `xsm` class is still on the element so it looks like it worked. We only caught it measuring `getComputedStyle`. Suggest emitting the size classes into `astryx-theme` too (or a later layer), or having `size` set a custom property the theme rule reads — or documenting that `size` and a themed `type` don't compose.
---
### Component-fit gaps (where we couldn't adopt, and kept our own)
**9. `ChatComposerInput` can't host a composer with platform-specific Enter behavior.**
It's a `contentEditable` div whose key handler is unconditional:
```js
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); … onSubmit?.(text); }
```
Our composer sends on Enter on a **desktop** but inserts a **newline** on a touch keyboard (a soft-keyboard Return mustn't strand a multi-line prompt), and guards IME composition. `ChatComposerInput` would send on Enter on a phone, with no prop to override it — and adopting it means trading a controlled-`value` + auto-grow `<textarea>` for `contentEditable` + `serialize()`, plus its built-in history/trigger model. A `contentEditable` input with mentions is a reasonable default; it just isn't a host for bespoke platform behavior, and there's no seam. Suggest an `onKeyDown` / `shouldSubmitOnEnter` hook, or letting `ChatComposer` accept a plain controlled `<textarea>` in its `input` slot without the contentEditable assumptions (today the shell's click-to-focus/submit wiring assume the default input). We migrated the composer's *controls* (Send/Mode/attach → `Button`/`IconButton`) cleanly and kept only the input.
**10. `Button` has a `destructive` variant but no constructive/success counterpart.**
`ButtonVariantMap` is `primary | secondary | ghost | destructive` — a red destructive with no green mirror. Migrating a permission grant/deny (approve = green, reject = red — a deliberate security-UX signal), we had `destructive` for Deny but nothing for Allow. `primary` recolors it to the brand and reads as "the main action" rather than "the affirmative/safe one", so we kept a small semantic tint over a `secondary` Button. Suggest a `constructive` (or `success`) variant mirroring `destructive`, drawing from the `--color-success` family the theme already defines — approve/reject, accept/decline, confirm/cancel are common enough that having one side but not the other pushes every consumer to recolor `secondary` by hand or misuse `primary`.
### Related upstream issue
One component-fit gap from this same migration that isn't in the 10 findings above — `CodeBlock` can't render per-line add/remove (diff) backgrounds — is already tracked in #3345. I've [added an external, real-world side-by-side there](https://github.com/facebook/astryx/issues/3345#issuecomment-5078376984) (our production diff viewer vs. `<CodeBlock highlightLines>`, light + dark), and folded the correction into the write-up's "surfaces we KEPT" table.
---
Environment: `@astryxdesign/core@0.1.8`, `@astryxdesign/theme-neutral@0.1.8`, `@astryxdesign/cli@0.1.8`; Next 16.2.7 (webpack), React 19.2.7, pnpm 10.29, Node 22. Each finding above was verified against the installed package. Thanks for open-sourcing this — the incremental-adoption story held up on a real app.
---
**Full write-up** (all findings with repro commands, measurements, before/after screenshots, and the "what worked well" list): [docs/astryx-migration.md](https://github.com/ejc3/remote-claw/blob/main/docs/astryx-migration.md) in the app we migrated.
Contributor guide
Research direction
Start with docs/astryx-migration.md and the linked findings, then choose one of the ten independently scoped problems rather than attempting the whole report. Read the named README Quick Start, `astryx docs getting-started` or `theme` output, `astryx theme build --help`, and the relevant component entry point; done should mean a focused issue or change with a reproducible result and verified behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, playwright, react, typescript, webpack
- Domain
- cli, design, documentation, frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100