Troubleshooting

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

Machine-translated, not yet reviewed. Read the original

Almost every breakage at the entrance is one of three things: a missing CSS import, a preset config that disagrees with itself, 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 foundation is not wired up: the tokens, the base layer and the themes arrive through the layer’s virtual module. Check that the entry point has both modules, in this order:

src/main.ts
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno:granular.css' // tokens, base, themes
import 'virtual:uno.css' // utility classes

If layer: 'granular' is not set in the preset options, there is no separate layer module and everything arrives in a single virtual:uno.css — then the second line is enough.

The colours are there, the layout is not

What you see. The component is coloured, but the elements are stacked in a column, the spacing has slipped and the icon overlaps the text.

Why. virtual:uno.css was forgotten. The utility classes the component templates are drawn with are generated into that one, while the granular layer carries the foundation only.

The styles are gone for one particular component

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

Why. One of two things:

  1. The component is missing from the preset’s components list. Add it — or drop the list entirely, and then everything reaches the sheet.
  2. granularContent(options) and presetGranularNode(options) were given different option objects. The extractor then scans something other than what the preset generates. Keep one object for both calls.

The spinner does not spin, hidden text is visible

What you see. animate-spin does nothing, dividers are not drawn, and service labels — a dialog heading, a table caption — are shown to the user as ordinary text.

Why. includeExtraRules is off. The components use utilities that presetMini does not have at all — sr-only, animate-spin, divide-*, space-*, backdrop-* — and the preset pulls them from a separate package. That is a condition of the thing working rather than a decoration: with includeExtraRules: false the class stays in the markup, the CSS never appears, and the build passes in silence.

The application throws on a dropdown import

What you see.

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

Why. The library is not installed. It is a required peer dependency: it is what positions GrSelect, GrDropdown, GrAutocomplete, GrTreeSelect, GrTooltip and GrPopover. The package keeps it external deliberately — otherwise an application that already uses floating-ui would end up with a second copy.

bash
yarn add @floating-ui/dom

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 UnoCSS utility, and your config generates it, not the package. You need presetIcons and an icon collection; without them the class stays a class and the build passes in silence. 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 and the build passes.

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 whose resolver is registered after the core one. The core resolver is greedy: it claims any name starting with Gr. Companion resolvers work from an explicit list and therefore go first;
  • the name is written in kebab case where the resolver expects Pascal. The resolver recognises components by their prefix, and the prefix has to match.

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, write to the repository discussions. A case that had to be worked through twice ends up on this page.

Last reviewed: 2026-09-01