Troubleshooting
Symptom, cause, fix. Collected from the cases that actually broke rather than from imaginary ones.
On this page
- The component is grey and bare
- The borders are gone, buttons are colourless, headings are all one size
- The styles are gone for one particular component
- A translucent background arrives opaque
- The build fails on an import from the package
- The build fails with ComponentRootImportError
- The build fails with InvalidConfigError
- There is space for the icon but no icon
- TypeScript cannot find the subpath types
- Auto-import does not fire
- The dark mode flashes on load
- The dark mode does not switch on
- An overlay panel is stuck in the wrong place
- Overlays are not found in tests
- None of the above
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 theme with its modes, the component styles and the utilities all arrive in a single virtual module, and it is your application that imports it, not an integration:
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.
@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.
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.
yarn add @floating-ui/domThe build fails with ComponentRootImportError
What you see.
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. In granum 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.
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.
Invalid granum config at 'appSource': unknown option — did you mean 'appSources'?Why. granum checks granum.config strictly: a typo in a key, a key of an
earlier version — theme and modes before granum 3.8 (now entries of the
themes list), previews or the v1 preset’s 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 an earlier 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.
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 theresolversarray, 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 theirexports. A name no package exports (a typo, a component of your own) is not resolved; GranularityResolverhas a customprefixorexclude, and the name does not fit it. Case does not matter: the plugin turns<gr-button>intoGrButtonby itself.
The dark mode 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
mode’s pair is set only after hydration. The cure is a script in <head> before
the first paint: granum’s plugin inserts it, and in Astro the
@feugene/astro-granularity integration does. How to switch it on is on the
SSR page.
The dark mode does not switch on
What you see. The switch changes data-theme, yet the components stay light.
Why. The root carries the mode name — data-theme="dark" — while the
selectors expect a pair of theme and mode: slate-dark. It happens when the
attribute is written by hand or your own CSS still expects [data-theme='dark'].
Switch with useTheme().setMode('dark'): the controller sets the pair from the
build’s manifest. Write your own selectors as [data-theme$='-dark']. If the
theme is different or the modes are your own, hand the virtual:granum/themes
manifest to initThemeEarly or granularityThemePlugin — without it
useTheme() writes slate-* pairs.
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. Clearing body
removes the nodes but not the package’s module state: an unanswered service dialog
from the previous test stays at the head of the queue, and a new one never appears;
the toast queue and the announcer’s timers survive the test too. 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.