# Migration

> Moving between majors: what the moves to 1.0, 2.0 and 3.0 broke and how to make them.

- Page: https://granularity.tech/docs/migration
- Last reviewed: 2026-10-27

**The library has had three major transitions.** From 2.x to 3.0 component values
moved into themes: token names are a contract of the core, and the theme supplies the
values. From 1.x to 2.0 came themes and modes: the theme became data of the token
package, light and dark became its modes, and the build moved to granum 3. From 0.x to
1.0 the pipeline the application builds its CSS with changed. All three moves are
breaking even though not a single component import changes in any of them.

## From 2.x to 3.0

Components, their props and the look of Slate do not change: the values components see
in every mode and island match 2.x. What changes is where these values live and who
answers for their completeness.

| In 2.x | In 3.0 |
|---|---|
| peer `@feugene/granum` `>=3.8.0 <4.0.0` | `>=3.10.0 <4.0.0`; the token package `@feugene/granularity-tokens` is major 2 |
| a granum config without `themes` built the catalog's default theme | `themes` is required: the application chooses the main theme, and without it granum fails with the list of catalog themes |
| `GR_DEFAULT_THEME_ID` | `GR_BASE_THEME_ID` — the package's base theme, not the main theme of someone's build |
| component token values lived with the components | the values live in the theme, in its `components/@feugene/granularity/<GrX>/` section |
| a component read a theme token with a fallback value | without one: a missing value is named by `granum doctor` (`theme-incomplete`) |
| `--gr-button-neutral-bg` was a hook the theme set | a theme token; a hook is the application's handle, and in theme data it is a `theme-token-foreign` finding |
| `theme.json` with modes as an array and `componentDefaults` at the root | modes as an object, component defaults in `extensions.granularity.componentDefaults`, a mode file holds flat values |

The application sets its own values on top of a theme as before — with
`modes.tokenOverrides` of the theme entry or its own CSS; a hook stays the application's
handle on top of the theme token.

**One thing breaks silently** — a theme of your own without `extends` that relied on the
components' fallback values. There is no build error: the component simply does not get
the token, and `granum doctor` names it. The values go into the theme's `components`
section, or the theme inherits a catalog theme; starting from a copy is easiest with
`gr-theme-copy slate <id>`. A theme with `extends: 'slate'` works as before and now also
gets the component values from Slate.

The `charts` and `media` companions moved to core 3 with 3.0 majors: their values live in
the theme too, and their core range is `>=3.1.0 <4.0.0`. `chrono`, `code`, `dashboard`,
`editor` and `forms-schema` keep no values in the theme and work with both core majors.
The full map of the move is in the
[library documentation](https://github.com/efureev/granularity/blob/main/packages/granularity/docs/migration-3.md).

## From 1.x to 2.0

The components, their props and the look of Slate do not change: the token values
match 1.x except for two fixed defects of the dark island, described in the
[changelog](/changelog). What changes is how the theme reaches the build and how
the mode is switched.

| In 1.x | In 2.0 |
|---|---|
| peer `@feugene/granum` of major 1 or 2 | `>=3.0.0 <4.0.0`, releases with build themes need `>=3.8.0 <4.0.0`; the `@feugene/granum-engine-wind` engine is major 3 too |
| `themes: { names: ['light', 'dark'] }` | `themes: [{ theme: 'slate', modes: ['light', 'dark'] }]` — the list of build themes; an object `themes` fails with `InvalidConfigError` |
| `themes.define`, `themes.tokenOverrides` | `modes.define`, `modes.tokenOverrides` in the theme's entry |
| `data-theme="dark"` on the root | the pair `data-theme="slate-dark"`; there are no `.light` and `.dark` classes |
| `[data-theme='dark'] .x` in your own CSS | `[data-theme$='-dark'] .x` or the full pair; `dark:` in utilities works unchanged |
| `useTheme().theme` — the mode name | `theme` — the current build theme; the mode is `mode` |
| `setTheme('dark')`, `toggleTheme()` | `setMode('dark')`, `toggleMode()`; `setTheme(id)` switches the build theme |
| the `ThemeName` type | `GrModeName`, `GrThemeId` |
| `<GrConfigProvider theme="dark">` | `<GrConfigProvider mode="dark">`; `theme` is an island of another build theme |
| `@feugene/granularity/theme`: `extendTheme`, `createTheme` | `@feugene/granularity-tokens/theme`: `extendMode`, `createMode`, `defineTheme` |
| `@feugene/granularity/theme/apply` | `@feugene/granularity-tokens/theme/apply` |
| `grThemeTokens` from `@feugene/granularity/tokens` | `grRoleTokens` from `@feugene/granularity-tokens` |
| `styles/tokens.css`, `styles/themes/light.css` and `dark.css` | `themes/slate.css` — both modes of one theme |

The mode manifest is handed over once, where the state is created:
`initThemeEarly({ manifest })` or `app.use(granularityThemePlugin, { manifest })`
with `manifest` from `virtual:granum/themes`. The storage key is unchanged —
`gr-theme` — and for the default theme it still holds the mode name: the user's
choice survives the upgrade. The token package needs no separate install — it arrives as a dependency
of the core.

**One thing breaks silently** — an attribute or a selector with a mode name
instead of a pair. `data-theme="dark"` set by hand and your own CSS under
`[data-theme='dark']` give neither an error nor a warning: the components simply
stay in the default mode. `theme="dark"` on `GrConfigProvider` at least gets a
warning in a dev build.

The first 2.x releases wrote the theme and the modes as separate keys — `theme: 'slate'`
and `modes: { names: [...] }`. Since granum 3.8 that is an entry of the `themes` list,
and `previews` and `css.previews` became additional themes with sheets of their own.
granum rejects the old keys with a config error that says where the value moved.

The companions moved to core 2 with their own 2.x majors on the same day: core 2
cannot be mixed with a companion built for core 1 — the manifests of granum 2 and 3
are incompatible. The full map of the move is in the
[library documentation](https://github.com/efureev/granularity/blob/main/packages/granularity/docs/migration-2.md), the granum config keys
are in its guide ([ru](https://github.com/efureev/granum/blob/main/docs/ru/migration.md),
[en](https://github.com/efureev/granum/blob/main/docs/en/migration.md)).

## From 0.x to 1.0

| In 0.x | In 1.0 |
|---|---|
| peer `@feugene/unocss-preset-granular` | peer `@feugene/granum` `>=1.0.0 <2.0.0` and the `@feugene/granum-engine-wind` engine |
| `uno.config.ts` | `granum.config.ts`; the engine is passed as an instance — `engine: windEngine()` |
| CSS concatenated in import order | CSS arrives as the `virtual:granum.css` module in five `granum.*` layers |
| the browser reset as plain CSS | the reset in a layer: `@import '…' layer(reset);`, or it overrides the library |
| `defineGranularComponent`, `defineGranularProvider` | `defineGranumComponent`, `defineGranumProvider` from `@feugene/granum/contract` — for provider authors |

The companions that depend on the core moved to 1.0 on the same day: their core
range is now `>=1.0.0 <2.0.0`, and it does not match a 0.x core. That is why the
core and the companions are upgraded together. The step-by-step guide is in the
granum documentation
([ru](https://github.com/efureev/granum/blob/main/docs/ru/migration.md),
[en](https://github.com/efureev/granum/blob/main/docs/en/migration.md)).
Breaking changes within the 0.x line are in the [changelog](/changelog).

## What will appear here by the next major

:::steps
1. **A map of API changes** — what was renamed, what was removed, what changed its default. As a table rather than as prose: prose cannot be searched by a prop name.
2. **A codemod.** Renames are performed by a script rather than by hand: it lives in the repository next to the migration and runs with one command.
3. **What breaks silently.** A separate section for changes that give neither a build error nor a warning — a shifted default, a different resolution order, another ARIA contract. This is the most expensive part of a move and the shortest one in other people's guides. One such case is already known from 1.0 — a reset outside the layer, see [troubleshooting](/docs/troubleshooting).
4. **The order of operations** for an application that does not upgrade all at once: what can stand side by side, what conflicts, where the compatibility line runs.
5. **An effort estimate** by application size — so that "move now or next quarter" is decided by a number rather than by mood.
:::

## Documentation versions

There is one set of documentation, and it describes the 3.x line; the portal
keeps no 0.x, 1.x or 2.x archive. The version in the header is read from the installed core's
manifest at build time and is never typed by hand — it matches what the portal is
built with.

The details of the version policy are on the [versioning](/docs/versioning)
page.

:::callout{type="info"}
Moving **from another library** is not migration in this sense. The component
maps for it are being prepared on a separate page —
[Migrating from…](/docs/migrating-from).
:::
