Troubleshooting

  • Upgrades and support
  • 9 min
  • 8 snippets

Symptom, cause, fix. Collected from the cases that actually broke rather than from imaginary ones.

Reviewed 2026-10-16

On this page

Almost every breakage at the entrance is one of three things: a missing CSS import, a reset declared in the wrong layer, or a missing required peer dependency in the project. Below, by symptom.

The component is grey and bare

What you see. A button with no colour, no radius, no padding — as if there were no CSS at all.

Why. The sheet is not imported. The tokens, the base layer, the themes, the component styles and the utilities all arrive in a single virtual module, and it is your application that imports it, not an integration:

src/main.ts
import './reset.css'
import 'virtual:granum.css'

The borders are gone, buttons are colourless, headings are all one size

What you see. It looks like the case above, except the CSS is clearly there: text and background colours are in place, and everything the library draws is not.

Why. The reset is declared after granum’s layers and therefore wins against the whole library. Unlayered CSS beats ANY @layer regardless of specificity, and a layer declared later beats one declared earlier. * { border-width: 0 } from the reset strips the borders, h1…h6 { font-size: inherit } the heading sizes, and button { background-color: transparent } from @unocss/reset/tailwind.css the buttons’ background as well. tailwind-compat.css comments that line out precisely for the sake of component libraries.

src/reset.css
@import '@unocss/reset/tailwind-compat.css' layer(reset);

The file must be imported first: a layer’s place in the cascade is fixed by its first appearance. Getting the order wrong produces neither an error nor a warning.

If the reset lives in a <style> inside a page template, the order of the links may not be the one you expect: the bundler inserts such a block after the <link> to the sheet. Then the order is set by declaration — a lone @layer reset; line in a file imported before everything else.

The styles are gone for one particular component

What you see. The rest are fine, and the newly added one is bare.

Why. The component is missing from the components list. Add it — or set 'all', and then everything reaches the sheet.

By default this does not stay silent: js.guard fails the build on an import of a component outside the selection. If you set it to 'warn' or 'off', the symptom goes back to the old one — a bare component with no message at all.

A translucent background arrives opaque

What you see. The overlay backdrop is opaque, bg-[var(--gr-bg)]/55 fills with a solid colour.

Why. The engine’s extraRules is off. preset-wind3 cannot apply alpha to an arbitrary colour in brackets: it emits background-color: var(--gr-bg), silently dropping the alpha — the class matched, the CSS is wrong, there is no error. The engine makes up for it with one rule on top of the preset, and it is on by default. The build stays silent, but granum doctor reports provider-dialect-mismatch: the package was built for the unocss/preset-wind3+granum@66 dialect, while the engine without the extra rules speaks unocss/preset-wind3@66 — a different vocabulary, even though the class names are the same. Dialects are described in the granum documentation (ru, en).

There used to be eight families of such rules — sr-only, animate-spin, divide-*, space-*, object-* and the rest — because the gaps being filled were preset-mini’s. In the preset-wind3 vocabulary seven of the eight are native, and only the alpha is left to make up for.

The build fails on an import from the package

What you see.

plaintext
Failed to resolve import "@floating-ui/dom"

Why. The library is not installed. It is a required peer dependency: it positions every floating panel — GrSelect, GrDropdown, GrDropdownMenu, GrContextMenu, GrAutocomplete, GrTreeSelect, GrTooltip, GrPopover — and everything that contains one: GrDataTable, GrPagination, GrColorPicker, GrSidebar. An import from the package root always pulls it in. This is a build error, not a runtime one: the dev server shows it as above, and under vite build it reads Rolldown failed to resolve import "@floating-ui/dom". The package keeps the library external deliberately — otherwise an application that already uses floating-ui would end up with a second copy.

bash
yarn add @floating-ui/dom

The build fails with ComponentRootImportError

What you see.

plaintext
ComponentRootImportError: Components 'GrButton', 'GrCard' are imported from the
package root '@feugene/granularity' in 'src/App.vue'. Import components from
'@feugene/granularity/components/GrButton', … or from 'virtual:granum/components'

Why. A component is imported from the package root: import { GrButton } from '@feugene/granularity'. granum’s selection only sees a component by its subpath, while the root pulls in the whole package — in dev and in SSR that is every one of its components. With granum 2 this is a guard error rather than a silent breakage. Utilities, plugins, constants and types may still come from the root — the rule only covers components.

ts
import { GrButton } from '@feugene/granularity/components/GrButton'

Or drop the import altogether and set up auto-import: the resolver inserts the subpath itself.

The build fails with InvalidConfigError

What you see.

plaintext
Invalid granum config at 'appSource': unknown option — did you mean 'appSources'?

Why. granum 2 checks granum.config strictly: a typo in a key, a v1 preset key (themes.themeFiles, scan) and a token with the -- prefix in tokenOverrides or define are errors naming the path to the field, not silently ignored settings. Fix the field the error names: for a typo it suggests the nearest key, for a v1 key it says where the key went.

There is space for the icon but no icon

What you see. An empty square where an icon passed as a class was expected (icon="i-lucide-user").

Why. The class i-lucide-* is a utility, and your config generates it, not the package. Icon rules are not part of the preset-wind3 vocabulary: without a rule the class stays a class, the space for the icon is there, the picture is not, and the build passes in silence.

The rule is handed to the engine factory — windEngine({ rules: [iconRule] }) — or brought in by a provider. The difference is described on the installation page: a rule from a provider leaves the application’s vocabulary fingerprint alone, while a rule from the config changes it and granum re-extracts every package’s classes. With one or two providers this goes unnoticed.

The alternative is to pass the icon as a Vue component, and then there is nothing to configure.

The package’s own icons — the select arrow, the clear cross, the check mark, the spinner — are compiled into dist and do not depend on your config.

TypeScript cannot find the subpath types

What you see.

plaintext
Cannot find module '@feugene/granularity/components/GrButton'
or its corresponding type declarations. ts(2307)

Meanwhile an import from the package root types fine — but granum rejects a component from the root as a build error, so that is no way out.

Why. "moduleResolution": "node" in tsconfig.json. The old algorithm does not read the exports field, and the subpath types are declared only there. You need "bundler" or "node16".

Auto-import does not fire

What you see. <GrButton> in a template turns into an unknown element and Vue complains in the console.

Why. One of three things:

  • the resolver is not wired into unplugin-vue-components — check the resolvers array, not just the plugin itself;
  • the component comes from a companion package that is missing from the application’s package.json: the resolver takes the packages from there and the components from their exports. A name no package exports (a typo, a component of your own) is not resolved;
  • GranularityResolver has a custom prefix or exclude, and the name does not fit it. Case does not matter: the plugin turns <gr-button> into GrButton by itself.

The dark theme flashes on load

What you see. A light background for a fraction of a second, then a dark one.

Why. The server (or the static HTML) does not know the user’s choice, and the theme attribute is set only after hydration. The cure is an inline script in <head> before the first paint — the ready-made code is on the SSR page.

An overlay panel is stuck in the wrong place

What you see. A dropdown panel or a modal positions itself relative to a container rather than to the window, and drifts on scroll.

Why. The container named in portalTarget has a transform, filter, contain, perspective or will-change. Any of them creates a containing block for position: fixed, and the panel starts measuring from the container. The requirement for a portal container is strict and reads exactly like that: not one of those properties.

Overlays are not found in tests

What you see. The test mounts a component, opens a panel — and does not find it in the document. No errors in the console.

Why. resetGranularityDom is not called between tests. The portal root and the live region host are cached by their modules: after document.body.innerHTML = '' they point at nodes outside the document, and the next mount teleports the overlays there. The details are on the testing page.

None of the above

If the symptom is not here, open an issue in the repository. A case that had to be worked through twice ends up on this page.