Configuration

  • Get started
  • 6 min
  • 5 snippets

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

Reviewed 2026-10-16

On this page

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

  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.

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

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

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:

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(...).