Migration

  • Upgrades and support
  • 7 min

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

Reviewed 2026-10-27

On this page

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.xIn 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 themethemes is required: the application chooses the main theme, and without it granum fails with the list of catalog themes
GR_DEFAULT_THEME_IDGR_BASE_THEME_ID — the package’s base theme, not the main theme of someone’s build
component token values lived with the componentsthe values live in the theme, in its components/@feugene/granularity/<GrX>/ section
a component read a theme token with a fallback valuewithout one: a missing value is named by granum doctor (theme-incomplete)
--gr-button-neutral-bg was a hook the theme seta 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 rootmodes 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.

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. What changes is how the theme reaches the build and how the mode is switched.

In 1.xIn 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.tokenOverridesmodes.define, modes.tokenOverrides in the theme’s entry
data-theme="dark" on the rootthe 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 nametheme — the current build theme; the mode is mode
setTheme('dark'), toggleTheme()setMode('dark'), toggleMode(); setTheme(id) switches the build theme
the ThemeName typeGrModeName, 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/tokensgrRoleTokens from @feugene/granularity-tokens
styles/tokens.css, styles/themes/light.css and dark.cssthemes/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, the granum config keys are in its guide (ru, en).

From 0.x to 1.0

In 0.xIn 1.0
peer @feugene/unocss-preset-granularpeer @feugene/granum >=1.0.0 <2.0.0 and the @feugene/granum-engine-wind engine
uno.config.tsgranum.config.ts; the engine is passed as an instance — engine: windEngine()
CSS concatenated in import orderCSS arrives as the virtual:granum.css module in five granum.* layers
the browser reset as plain CSSthe reset in a layer: @import '…' layer(reset);, or it overrides the library
defineGranularComponent, defineGranularProviderdefineGranumComponent, 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, en). Breaking changes within the 0.x line are in the changelog.

What will appear here by the next major

  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.
  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 page.

Moving from another library is not migration in this sense. The component maps for it are being prepared on a separate page — Migrating from….