Configuration

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

Machine-translated, not yet reviewed. Read the original

Configuration here has two layers and they do not overlap. At build time you configure what reaches the CSS — that is uno.config.ts 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.

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 on first open.

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 the ones that do carry it inside their chunk, so it arrives with the import.

The core resolver is greedy — it claims any name starting with Gr. Companion package resolvers work from an explicit list and are therefore registered before it: GranularityChronoResolver() first, GranularityResolver() last.

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

Last reviewed: 2026-09-01