Getting Started
Upgrading from 1.5
For apps on @nexub/* 1.5.x. Nothing was removed and no prop was renamed, but 1.6 and 1.7 changed some markup and roles that tests and selectors see, and added conventions worth adopting. Every release since (1.8 through 2.x) is a rolling minor with no breaking changes, so this guide takes you straight to the current version.
1. Update
All @nexub/* packages share one version; update them together.
pnpm add @nexub/react@^2 @nexub/patterns@^2 @nexub/themes@^2 @nexub/tokens@^2 @nexub/icons@^2Peer dependencies are unchanged: react and react-dom 18 or 19, and @nexub/tokens. Keep the Tailwind preset and content globs from Getting Started.
2. Fix tests and selectors
| Component | Before (1.5) | After (1.6+) | What to do |
|---|---|---|---|
| ToggleGroup, SegmentedControl | role="group", items with aria-pressed | single-select: role="radiogroup", items role="radio" + aria-checked; multi-select: role="toolbar" | Query getByRole('radiogroup') / getByRole('radio', { name }) and assert aria-checked. |
| Button loading (and status="loading") | native disabled; focus lost | aria-disabled="true"; focus stays, clicks are blocked, the change is announced; width holds | Assert toHaveAttribute('aria-disabled', 'true') instead of toBeDisabled(). Don’t rely on focus moving away. |
| FileUpload dropzone | div role="button" with nested buttons | a native <label> wrapping the file input; one control named by its visible text | Query getByLabelText(…) (the visible text), not getByRole('button'). userEvent.upload on that input. |
| ActivityFeed | role="feed" | a plain list (ul / li) | Query getByRole('list') / getAllByRole('listitem'). |
| Avatar without name | role="img" with no name | aria-hidden (decorative) | Pass name when the avatar carries meaning; otherwise stop querying it by role. |
| Heading, Label, HelperText tone | inline style="color: …" | a colour class (no style attribute) | Snapshot / toHaveStyle checks on colour change; assert the class or computed colour. |
| Stack, Flex, Grid | inline style for gap, align, justify, cols, rows | Tailwind classes for on-scale values; off-scale values still use inline style | Update snapshots. Use scale values (gap={4}) to keep server HTML style-free. |
| Brand tones (leap, bookub, payub, founderub) | fell back to the default text colour | tint the text in Heading, Label, HelperText and EyebrowChip | Check contrast where you used a brand tone and expected neutral text. |
| Toaster | every toast listed | newest on top, older ones stacked behind; fan out on hover/focus; closed toasts leave the store | Pass expand to keep them fanned out; visibleToasts sets how many render (default 3). Tests that count toasts: count open ones. |
| tailwindcss-animate | components used animate-in / fade-in classes | components animate with nx-motion (themes/base.css) | Nothing for components. The preset still registers the plugin for your own classes; prefer nx-motion. |
// 1.5
screen.getByRole('group', { name: 'View' });
expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
// 1.6+
screen.getByRole('radiogroup', { name: 'View' });
expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute('aria-disabled', 'true');3. Adopt the new conventions
Colour roles
Use role names (--color-surface, --color-on-surface, --color-primary, *-container, outline…) and the Tailwind colours of the same name. The legacy names in @nexub/themes/light still work as aliases, so this can be gradual. Colors lists every role.
| Legacy | Role |
|---|---|
--color-bg-default / -subtle / -muted | --color-surface / -surface-container-low / -surface-container-high |
--color-bg-raised, --color-surface-raised | --color-surface-elevated |
--color-text-primary / -secondary / -muted | --color-on-surface / -on-surface-variant / -on-surface-muted |
--color-text-link | --color-primary |
--color-border-default / -strong / -input | --color-outline-variant / -outline / -outline |
--color-semantic-{tone}-default | --color-{tone} |
--color-semantic-{tone}-bg / -strong | --color-{tone}-container / -on-{tone}-container |
--color-bg-inverse / --color-text-inverse | --color-inverse-surface / -inverse-on-surface |
Brand accents are roles too: --color-leap, --color-on-leap, --color-leap-container (same for bookub, payub, founderub). For charts use --color-chart-1..8 (categorical), chart-seq-1..7 and chart-div-1..7.
Logical classes for right-to-left
Components flip with dir; your layout should too. Replace ml-/mr- with ms-/me-, pl-/pr- with ps-/pe-, left-/right- with start-/end-, border-l/-r with border-s/-e, rounded-l/-r with rounded-s/-e, and text-left/-right with text-start/-end.
NexubProvider
One provider for locale (Intl formatting), dir, messages (every built-in string) and dateLocale (date-fns locale for the pickers). Optional; without it you get English and left-to-right. See Internationalization.
<NexubProvider locale="fr" messages={fr} dateLocale={frLocale}>
{children}
</NexubProvider>Strict CSP
Server HTML from layout and text components carries no style attribute when you use scale values, so style-src 'self' works without 'unsafe-inline'. Pass scrollLock="class" to Sheet/Drawer, and a nonce to ThemeScript. Details in Strict CSP.
Motion tokens
Animate your own overlays with nx-motion nx-pop (or nx-dialog) on Radix's data-state instead of animate-in classes, and time transitions with the spring tokens: duration-spatial-default ease-spatial-default for movement, duration-effects-fast ease-effects-fast for opacity and colour. Exits should be faster than enters, and every animation needs a motion-reduce: fallback. See Motion & interaction.
Touch targets
Button, IconButton, Checkbox, Radio and Switch get a 44×44px hit area on touch screens. Add nx-hit to your own small controls and keep 44px between their centres.
4. Automate it: nexub-migrate
@nexub/tokens 1.8.0 ships a codemod. It is a dry run by default and prints what it would change.
npx nexub-migrate src # dry run: report changes
npx nexub-migrate src --write # apply them
npx nexub-migrate src --check # CI: exit 1 if anything is left| Rule | What it does |
|---|---|
legacy-tokens | Legacy colour names → roles, in CSS variables and Tailwind classes. |
logical-classes | Physical Tailwind classes (ml-, pr-, left-, text-left…) → logical ones. |
tw-animate | Reports tailwindcss-animate classes to replace with nx-motion. Report only. |
opacity-modifier | Opacity modifiers on role colours (bg-primary/50, bg-[var(--color-primary)]/50), which Tailwind 3 silently drops. |
Run your tests after --write, and review the diff: it rewrites class strings, not logic.