# Configuration

> Defaults for a subtree through GrConfigProvider, the order they resolve in, auto-import and the runtime adapter.

- Page: https://granularity.tech/docs/configuration
- Last reviewed: 2026-10-16

Configuration here has two layers and they do not overlap. At build time you
configure **what reaches the CSS** — that is `granum.config.mjs` and the
[installation](/docs/installation) page. At runtime you configure **how
components behave by default** — and that is `GrConfigProvider`, which this whole
page is about.

## Defaults for a subtree

```vue
<GrConfigProvider
  size="sm"
  :component-defaults="{ GrButton: { variant: 'outline' }, GrInput: { clearable: true } }"
>
  <App />
</GrConfigProvider>
```

The provider renders transparently (`display: contents`) and works through
`provide`/`inject`, so it changes no layout and can sit anywhere — including
nested several times over. A child provider merges over its parent **at the prop
level**, not at the component block: override one `variant` and you do not lose
the rest of the `GrButton` defaults.

## The resolution order

:::steps
1. A local prop on the component itself.
2. `componentDefaults[Component]` of the nearest provider.
3. The provider's global `size`.
4. The component's own default.
:::

Step 3 applies only to the `size` prop of controls: overlays have a scale of
their own, and the global `size` leaves them alone. A button inside a
`GrButtonGroup` takes the group's value before the config.

Which gives a rule worth knowing as a consumer too: a prop configurable through
the provider is declared with a default of `undefined`. Otherwise Vue would
substitute its value before the component ever looked into the config, and the
provider would quietly stop working.

The effective config can be read from the application with `useGrConfig()` — that
is public API, not an internal.

## Two size scales

The provider's `size` is about **controls**: `xs | sm | md | lg`. Overlays have a
scale of their own — `sm | md | lg | xl | full` — and the global `size` does not
touch it, because `xs` means nothing for a modal window. Window size is set
pointwise:

```vue
<GrConfigProvider :component-defaults="{ GrModal: { size: 'lg' } }">
```

That is how `GrModal`, `GrDialog`, `GrConfirmDialog`, `GrPromptDialog`,
`GrCommandPalette` and `GrDrawer` are configured.

## A theme for a subtree

`theme` puts its value into `data-theme` on the wrapper. Themes are declared with
an attribute selector, so a dark island inside a light application works with no
extra styles.

:::callout{type="info"}
Overlay panels teleport into `body` and live outside the wrapper in the DOM — but
in the component tree they stay inside, so `inject` reaches them and they set the
theme on themselves. The modal, the drawer, the dropdown, the popover, the
tooltip, the selects, the toaster and the image viewer are all covered.
:::

The **document** theme is a different job: `useTheme` and `initThemeEarly` do it,
see [theming](/docs/theming). The package has no two mechanisms for one thing,
and the provider's prop is about the island.

## The layering scale and the mount point

`zIndexBase` recomputes `--gr-z-*` from a base (`dropdown` +0, `tooltip` +50,
`modal` +100, `toast` +200) and sets them **on `<html>`**, restoring the previous
values on unmount. On `:root` rather than on the wrapper — for the same reason
panels set their own theme: a panel moves into `body` and never sees the
subtree's variables. There is one scale per document, and a second provider with
a different base warns about the conflict in a dev build.

The same result is four lines of CSS. The prop is for where the base comes from
at runtime — a micro-frontend inside somebody else's application, for instance.

`portalTarget` names the container the subtree's overlays move into. By default
that is the shared `#gr-portal` in `body`, which the package creates itself the
first time an overlay renders in the browser.

:::callout{type="warning"}
The provider only names the target — it does not create the DOM, and the
container has to exist by the time an overlay opens. There is one requirement for
it, and it is strict: **no `transform`, `filter`, `contain`, `perspective` or
`will-change`.** Each of them creates a containing block for `position: fixed`,
and the floating panels start measuring from the container instead of the
viewport.
:::

## Auto-import

The recommended way for a production application is the resolver for
`unplugin-vue-components`. It inserts static imports on the package's subpaths
into the SFC, so tree-shaking stays tight: the bundle gets exactly what appeared
in the templates.

```bash
yarn add -D @feugene/unplugin-granularity unplugin-vue-components
```

```ts title="vite.config.ts"
import Components from 'unplugin-vue-components/vite'
import { GranularityResolver } from '@feugene/unplugin-granularity'

export default defineConfig({
  plugins: [vue(), Components({ resolvers: [GranularityResolver()] })],
})
```

No CSS needs wiring up for this: most components have no CSS of their own, and
for the rest (22 of 84) granum collects it from the package manifest into the
`granum.components` layer of `virtual:granum.css`. Component chunks import no
CSS, so leave `importStyle` off on the core resolver: the package publishes no
such subpath.

:::callout{type="info"}
One resolver serves the core and every companion package: it finds the packages in
the application's `package.json` and the components in their `exports`, so the order
of resolvers does not matter. A part of a compound component (`GrListItem`) comes
from its parent's subpath, and a name no package exports is not resolved at all —
instead of an import of a path that does not exist, Vue warns about an unknown
component.
:::

## The runtime adapter

The resolver scans templates. Directives applied in `render`, JSX or TSX are
invisible to it — `createGranularity` is for those:

```ts title="src/main.ts"
import { createApp } from 'vue'
import { createGranularity } from '@feugene/granularity/vue'
import { GrButton } from '@feugene/granularity/components/GrButton'
import { vHotkey } from '@feugene/granularity/directives'
import App from './App.vue'

createApp(App)
  .use(createGranularity({
    components: [GrButton],
    directives: [{ name: 'hotkey', directive: vHotkey }],
  }))
  .mount('#app')
```

The adapter imports no component itself — it takes them as an argument. The
bundle gets exactly what you passed and nothing beyond. The flip side of the same
property: passing "everything" here means losing granularity, and tree-shaking
will not save you, because the import was yours.

Beyond components and directives the adapter takes `provides` and
`globalProperties` — one convenient entry point for a bootstrap file instead of a
scattering of `app.use(...)`.
