Skip to main content
Urbicon UI

BlocksProvider

One context provider for app-wide styling: round every Card, register a named stat-card look, add a border only on variant="outlined", or strip all default styles and bring your own.

Global Component Defaults

The provider is optional: components carry their full default look without it. Wrap your app once to change that look globally, passing per-component slotClasses via the defaults prop. Keys are the exported component names, case-sensitive; an unmatched key is ignored without a warning (one exception: ConfirmDialog registers under the Dialog key).

Setting global defaults

<!-- src/routes/+layout.svelte -->
<script>
  import { BlocksProvider } from '@urbicon-ui/blocks';
  let { children } = $props();
</script>

<BlocksProvider
  defaults={{
    Button: {
      slotClasses: {
        base: 'rounded-full font-bold uppercase tracking-wide'
      }
    },
    Card: {
      slotClasses: {
        base: 'rounded-3xl shadow-2xl'
      }
    }
  }}
>
  {@render children()}
</BlocksProvider>

Merge Behavior

Defaults, presets and instance slotClasses are merged per slot, each source stripping the earlier ones' conflicting Tailwind utilities, so the later source wins and non-conflicting classes accumulate. That merged string and the instance class prop then reach the component's tv() slot as two sources, in that order, and the pair strips the library's own conflicting defaults. So class beats every rung below it — the library default, a provider default, a preset and an instance slotClasses alike. Its one limit is reach: it lands on the root slot, so an inner element still needs slotClasses. All three buttons below live inside one provider that defaults Button to rounded-none:

Override behavior

<BlocksProvider defaults={{ Button: { slotClasses: { base: 'rounded-none' } } }}>
  <Button>Default (square)</Button>
  <Button slotClasses={{ base: 'rounded-full' }}>slotClasses beats the default</Button>
  <Button class="rounded-full tracking-widest uppercase">class beats it too</Button>
</BlocksProvider>

The live snippets on this page show markup only. Each needs the components it renders imported alongside the provider, e.g. import { BlocksProvider, Button } from '@urbicon-ui/blocks';.

Priority (lowest to highest):
  1. tv() variant styles (library default)
  2. BlocksProvider defaults.slotClasses
  3. BlocksProvider defaults.overrides[match]
  4. preset.slotClasses (when preset="…" is set)
  5. preset.overrides[match]
  6. Instance slotClasses prop
  7. Instance class prop (root slot only — the strongest rung)

Presets

defaults apply globally to every instance. Presets are different: register named looks once, then opt-in per component via the preset="name" prop, for looks that fall outside the semantic intent palette but should stay reusable across the project. An unregistered preset name warns in the browser console in dev and falls through to the provider defaults.

Why presets over class?
  • Reusable: define once, opt-in everywhere with a short name.
  • Maintains slot-level control: hover/focus/dark-mode logic stays inside the slotClasses map, not scattered across instance class strings.
  • Composes with intent: a card with preset="stat-tile" still honors intent, size, etc.

Two curated presets

Round-Icon-Tile (sub-cards in a dashboard) and Stat-Tile (compact KPI tiles).
12 buildings

Revenue

€42.1k

<BlocksProvider
  presets={{
    Card: {
      'round-icon-tile': {
        slotClasses: {
          base: 'rounded-2xl border-0 bg-primary-subtle',
          content: 'flex items-center gap-3 p-4'
        }
      },
      'stat-tile': {
        slotClasses: {
          base: 'rounded-xl shadow-sm',
          content: 'p-5'
        }
      }
    }
  }}
>
  <Card preset="round-icon-tile" padding="none">
    <div class="text-primary">
      <BuildingIcon size={20} />
    </div>
    <span class="text-text-primary text-sm font-medium">12 buildings</span>
  </Card>

  <Card preset="stat-tile" padding="none">
    <p class="text-text-tertiary text-xs">Revenue</p>
    <p class="text-text-primary text-2xl font-semibold">€42.1k</p>
  </Card>
</BlocksProvider>

The full type definitions:

ComponentDefaults and PresetMap

// packages/blocks/src/lib/provider/blocks-context.ts

export interface ConditionalOverride {
  class: Record<string, string>;            // slot → classes
  [propCondition: string]: string | string[] | boolean | Record<string, string> | undefined;
}

export interface ComponentDefaults {
  slotClasses?: Record<string, string>;     // unconditional
  overrides?: ConditionalOverride[];        // prop-conditional
}

export interface ComponentPreset {
  slotClasses?: Record<string, string>;     // unconditional
  overrides?: ConditionalOverride[];        // prop-conditional
}

// Outer key  = component name (e.g. 'Card', 'Spinner', 'Button')
// Inner key  = preset name (whatever the consumer types into preset="...")
export type PresetMap = Record<string, Record<string, ComponentPreset>>;

Conditional Defaults (overrides)

slotClasses apply to every instance regardless of variant. When a rule must target a specific variant / intent / state, e.g. a 1px border only on the outlined variant, use overrides. Each entry is a compoundVariant-shaped matcher (prop conditions → per-slot classes); on a match its classes join the cascade, where the tv() conflict resolver strips the library's conflicting class (here the outlined variant's border-2).

Style only the outlined variant

Entries match active prop values, so it is irrelevant whether the library defines border-2 in a variant or a compoundVariant. string = equals, string[] = one-of, boolean where the component carries a boolean; the primitives hold disabled, readonly and error as undefined when off, so only their true side matches. Multiple matches merge additively.
1px border untouched
<BlocksProvider
  defaults={{
    Badge: {
      slotClasses: { base: 'tracking-wide' },
      overrides: [{ variant: 'outlined', class: { base: 'border' } }]
    }
  }}
>
  <Badge variant="outlined">1px border</Badge>
  <Badge variant="filled">untouched</Badge>
</BlocksProvider>

When to reach for which of the three: the Customization hub's decision table settles it in one look.

Global Unstyled Mode

Set unstyled to strip all default styles from every component. They render their HTML structure but no visual styling. This is useful when building a completely custom design system on top of Urbicon UI components.

Unstyled mode

<script>
  import { BlocksProvider, Button, Card, Input } from '@urbicon-ui/blocks';
</script>

<!-- Strip all default styles globally -->
<BlocksProvider unstyled>
  <!-- Components render only HTML structure -->
  <Button class="my-custom-btn">Click me</Button>
  <Card class="my-custom-card">Content</Card>
</BlocksProvider>

Combine unstyled with defaults to build a complete custom design system. Everything below renders live with every library default stripped; the brutalist look is carried by the two slotClasses maps:

Unstyled + custom defaults (Brutalist example)

<BlocksProvider
  unstyled
  defaults={{
    Button: {
      slotClasses: {
        base: 'inline-flex items-center gap-2 rounded-none border-2 border-current px-6 py-3 font-mono text-sm font-bold tracking-widest uppercase transition-colors hover:bg-current/10',
        content: 'flex items-center gap-2'
      }
    },
    Input: {
      slotClasses: {
        base: 'w-full border-2 border-current bg-transparent px-4 py-3 font-mono focus-visible:outline-none',
        label: 'font-mono text-xs uppercase tracking-widest mb-1'
      }
    }
  }}
>
  <Input label="Callsign" placeholder="ORBIT-7" />
  <Button>Transmit</Button>
</BlocksProvider>

Props

PropTypeDefaultDescription
unstyledbooleanfalseStrip all default styles from all child components. They render only their HTML structure.
defaultsRecord<string, ComponentDefaults>{}Per-component defaults. slotClasses apply to every instance; overrides are prop-conditional rules (e.g. only variant="outlined"). Keys are component names (e.g. "Button", "Card").
presetsPresetMap{}Named looks per component, opt-in via the preset="name" prop. Each preset may carry its own conditional overrides.
childrenSnippet-Child content (your app).

Slot Names Reference

Each component defines its own set of named slots: the keys slotClasses accepts. The authoritative slot map lives in each component's API reference, on the slotClasses prop row (generated from the component's tv() config, so it cannot go stale), and your editor autocompletes the same keys. For example: Card, Input.

Reading a slot map

// Two examples; every component's API reference documents its own
// slot map on the slotClasses prop (derived from its tv() config).

// Card
slotClasses?: { base?: string; header?: string; content?: string; footer?: string }

// Input — the root slot is `wrapper`, the real <input> is `base`
slotClasses?: {
  wrapper?: string; container?: string; base?: string;
  label?: string; message?: string; /* … icon slots */
}