Skip to main content
Urbicon UI

Customization

Every component can be restyled from the outside. Start with the narrowest tool that solves your problem: class and slotClasses change one instance, BlocksProvider changes every instance, unstyled drops the library's styling and hands you the markup.

Which tool do I use?

I want to…Reach forExample
Restyle one element on one instance Beats every other rung in a shared Tailwind bucket — the library default, a provider default, a preset and slotClasses alike. It reaches the root slot only, so an inner element still needs slotClasses. See the trap below.class<Button class="rounded-full">
Restyle an inner element (the <input> itself, a header, a chevron…) Type-safe: autocomplete lists the available slot names for each component.slotClasses.<slot><Input slotClasses={{ base: "rounded-full" }} />
App-wide look for a component type (every Button, every Card) defaults apply to every instance; presets are opt-in via preset="name". Keys inside defaults are plain strings; the slot-name autocomplete lives on the component's own slotClasses prop.preset / BlocksProvider defaults<BlocksProvider defaults={{ Button: { slotClasses: { base: "rounded-full" } } }}>
Style only one variant / intent / state (e.g. only outlined) Prop-conditional rule matching any combination of variant props: only variant="outlined", or size and intent together.overridesdefaults={{ Badge: { overrides: [{ variant: "outlined", class: { base: "border" } }] } }}
Rebuild a component from scratch (strip every default) Renders the HTML structure only: you own every visual, including dark mode, hover/active and focus rings. On a composing component (DatePicker, ChatMessage, CommandPalette…) it also strips the blocks components it renders itself; anything you pass in as children keeps its look.unstyled + slotClasses<Card unstyled slotClasses={{ base: "…" }} />
Full precedence chain (weakest → strongest): Each source strips the earlier ones' conflicting Tailwind utilities on the slot they share, so the later source wins (an instance slotClasses rounded-none defeats a provider default rounded-full), and non-conflicting classes accumulate. Step 7 is a rung like the rest: an instance class="rounded-full" defeats a slotClasses rounded-none on the same slot. Its one limit is reach — class lands on the root slot only, so an inner element still needs slotClasses.
  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)

The class Root-Slot Trap

The class prop only reaches the outermost (root) slot. Most components wrap several elements. class lands on the root wrapper, not the element you are usually picturing. To style something inside, go through slotClasses.<slot>.

Input is the one that catches people: its root slot is wrapper (the label + field column), and the <input> itself is the base slot. So class="rounded-full" rounds the column, not the field.

class vs. slotClasses on Input

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

<!-- ❌ Surprise: this rounds the WRAPPER (label + field column), not the field. -->
<Input label="Email" class="rounded-full" />

<!-- ✅ Reach the actual <input> via the `base` slot. -->
<Input label="Email" slotClasses={{ base: 'rounded-full' }} />

slotClasses is type-safe on every component: the keys are derived from the component's tv() slots, so your editor autocompletes the available slot names (wrapper, container, base, label, message… for Input). Each component's API reference documents its slot map on the slotClasses prop (for example Input).

Theming

A theme is one CSS file, imported after the base styles. It replaces the primary and secondary accent ramps plus the neutral ramp (--color-neutral-*) that the surface, text and border tokens are built from, so a warm accent gets warm surfaces.

Use a built-in theme

/* app.css */
@import '@urbicon-ui/blocks/style/index.css';
@import '@urbicon-ui/blocks/style/themes/ocean.css';
Ocean
Forest
Sunset
Rose
Neutral

A brand color alone is not enough: the neutral ramp follows the accent, and any intent ramp (success, warning, danger) that lands near your brand hue has to be moved so status colors stay distinguishable. The full recipe, typography, a scoped-theme pattern and the dark-mode wiring are on Themes; the Theme Builder generates the file from your brand color.

Defaults & Unstyled Mode

When CSS tokens are not enough, wrap your app once in BlocksProvider:

The smallest provider setup

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

<BlocksProvider defaults={{ Button: { slotClasses: { base: 'rounded-full' } } }}>
  {@render children()}
</BlocksProvider>

Its defaults restyle every instance of a component type, named presets are opt-in looks per instance, and overrides apply to one combination of variant props. All three sit below instance props in the precedence chain. unstyled sits outside it: it drops the tv() styles, and whatever you pass is all that is left. The full API with worked examples is BlocksProvider.

Deep Dives