# 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-16

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:

```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. 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](/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 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](/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 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](/docs/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](/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.
