# Troubleshooting

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

- Page: https://granularity.tech/docs/troubleshooting
- Last reviewed: 2026-10-20

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:

```ts title="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.

```css title="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.

:::callout{type="info"}
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](https://github.com/efureev/granum/blob/main/docs/ru/engines-and-dialects.md),
[en](https://github.com/efureev/granum/blob/main/docs/en/engines-and-dialects.md)).

:::callout{type="info"}
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.

```bash
yarn add @floating-ui/dom
```

## The 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.

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

Or drop the import altogether and set up [auto-import](/docs/configuration): 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](/docs/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.

:::callout{type="info"}
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 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 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](/docs/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](/docs/testing) page.

## None of the above

If the symptom is not here, open an
[issue in the repository](https://github.com/efureev/granularity/issues). A
case that had to be worked through twice ends up on this page.
