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';.
tv() variant styles (library default)BlocksProvider defaults.slotClassesBlocksProvider defaults.overrides[match]preset.slotClasses (when preset="…" is set)preset.overrides[match]Instance slotClasses propInstance 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.
class? - Reusable: define once, opt-in everywhere with a short name.
- Maintains slot-level control: hover/focus/dark-mode logic stays inside the
slotClassesmap, not scattered across instance class strings. - Composes with intent: a card with
preset="stat-tile"still honorsintent,size, etc.
Two curated presets
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
<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
| Prop | Type | Default | Description |
|---|---|---|---|
unstyled | boolean | false | Strip all default styles from all child components. They render only their HTML structure. |
defaults | Record<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"). |
presets | PresetMap | {} | Named looks per component, opt-in via the preset="name" prop. Each preset may carry its own conditional overrides. |
children | Snippet | - | 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 */
}