Customization
Every Urbicon UI component is restyleable through one predictable ladder of escape hatches. Pick the lowest rung that solves your problem — lower rungs preserve more of the design system's behavior (dark mode, hover/active cascade, focus rings).
Which tool do I use?
Start from your goal, not from the API. Find the row that matches what you want to change — the Reach for column is the tool to use.
| I want to… | Reach for | Example |
|---|---|---|
| Restyle one element on one instance Highest priority. Merges onto the OUTERMOST (root) slot only — see the trap below. | class | <Button class="rounded-full"> |
| Restyle an inner element (the actual <input>, 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". | preset / BlocksProvider defaults | defaults={{ Button: { slotClasses: { base: "rounded-full" } } }} |
| Style only one variant / intent / state (e.g. only outlined) Prop-conditional rule — what unconditional slotClasses cannot express. | overrides | overrides: [{ variant: "outlined", class: { base: "border" } }] |
| Rebuild a component from scratch (strip every default) Renders the HTML structure only; you own all visuals. | unstyled + slotClasses | <Card unstyled slotClasses={{ base: "…" }} /> |
rounded-none defeats a default rounded-full); non-conflicting classes accumulate. 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 class Root-Slot Trap
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>.The classic surprise is Input: its root slot is wrapper (the label + field column), and the real <input> element 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 now 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). Check a component's API reference, or the Slot Names reference, for its slot map.
CSS Token Themes
The simplest way to brand the library. Import a theme CSS file after the base styles to override the primary and secondary accents plus the neutral chassis. The semantic layer (surface, text, border tokens) derives from that chassis, so each theme re-tints it to match the accent's temperature — warm accents get warm surfaces, not cold grey ones.
Use a built-in theme
/* app.css */
@import '@urbicon-ui/blocks/style/index.css';
@import '@urbicon-ui/blocks/style/themes/ocean.css';You can also create your own. Use the Theme Builder to generate a matched OKLCH palette, or follow the annotated walkthrough on CSS Token Themes.
--color-primary-* and nothing else, and your warm brand
button ends up sitting on cool blue-grey cards — surface-*, text-* and border-* derive from the neutral chassis, not from primary. A real theme also re-tints --color-neutral-* to the accent's temperature, and re-tunes any
intent your accent collides with (a green brand vs. success, an
amber one vs. warning).Rather than paraphrase that, here is a shipped theme in full — this is blocks/style/themes/forest.css itself, read straight from the package. Note what it does beyond the two accent ramps: the chassis
re-tint, the two collision re-tunes (green primary pushes success off 140;
lime secondary pushes warning off 80),
and the two :root chroma
knobs at the end.
forest.css — a complete theme, verbatim from the package
/*
* @urbicon-ui/blocks – Forest Theme
*
* Earthy green palette inspired by natural environments.
* Import AFTER the base styles:
*
* @import '@urbicon-ui/blocks/style/index.css';
* @import '@urbicon-ui/blocks/style/themes/forest.css';
*/
@theme {
--color-primary-50: oklch(0.95 0.03 155);
--color-primary-100: oklch(0.9 0.05 155);
--color-primary-200: oklch(0.82 0.08 155);
--color-primary-300: oklch(0.74 0.1 155);
--color-primary-400: oklch(0.66 0.12 155);
--color-primary-500: oklch(0.58 0.13 155);
--color-primary-600: oklch(0.5 0.13 155);
--color-primary-700: oklch(0.42 0.11 155);
--color-primary-800: oklch(0.34 0.09 155);
--color-primary-900: oklch(0.26 0.06 155);
--color-primary-950: oklch(0.17 0.04 155);
--color-secondary-50: oklch(0.95 0.02 90);
--color-secondary-100: oklch(0.9 0.04 90);
--color-secondary-200: oklch(0.82 0.06 90);
--color-secondary-300: oklch(0.74 0.08 90);
--color-secondary-400: oklch(0.66 0.1 90);
--color-secondary-500: oklch(0.55 0.08 90);
--color-secondary-600: oklch(0.48 0.08 90);
--color-secondary-700: oklch(0.4 0.07 90);
--color-secondary-800: oklch(0.32 0.05 90);
--color-secondary-900: oklch(0.25 0.04 90);
--color-secondary-950: oklch(0.18 0.03 90);
/*
* Chassis neutral ramp — re-tinted toward a green-grey stone (Hue 150) so
* surfaces, text and borders read as natural/earthy rather than cool blue
* (the library default neutral sits at Hue 240). Lightness and chroma
* match the foundation ramp one-to-one — only the hue shifts — so WCAG
* contrast stays unchanged. surface-*, text-* and border-* pick this up
* automatically through semantic.css.
*/
--color-neutral-25: oklch(0.985 0.003 150);
--color-neutral-50: oklch(0.965 0.006 150);
--color-neutral-100: oklch(0.95 0.008 150);
--color-neutral-200: oklch(0.89 0.012 150);
--color-neutral-300: oklch(0.83 0.014 150);
--color-neutral-400: oklch(0.7 0.015 150);
--color-neutral-500: oklch(0.55 0.016 150);
--color-neutral-600: oklch(0.42 0.017 150);
--color-neutral-650: oklch(0.38 0.016 150);
--color-neutral-700: oklch(0.32 0.016 150);
--color-neutral-750: oklch(0.28 0.014 150);
--color-neutral-800: oklch(0.23 0.015 150);
--color-neutral-850: oklch(0.18 0.014 150);
--color-neutral-900: oklch(0.15 0.012 150);
--color-neutral-950: oklch(0.08 0.008 150);
/*
* Intent re-tuning — the green primary (Hue 155) collides with the default
* success (Hue 140), and the lime secondary (Hue 90) collides with the
* default warning (Hue 80). Success is pushed to a teal-green (Hue 172) so a
* "saved" state reads as verification rather than brand; warning is pulled
* to amber (Hue 60) so it leaves the lime register. Same L/C profile as the
* foundation ramps; only the hue shifts. Danger (25) and info (220) keep the
* library defaults. Derived --color-success / --color-warning re-resolve at
* :root automatically (global @theme).
*/
--color-success-50: oklch(0.95 0.03 172);
--color-success-100: oklch(0.9 0.05 172);
--color-success-200: oklch(0.82 0.08 172);
--color-success-300: oklch(0.74 0.11 172);
--color-success-400: oklch(0.62 0.14 172);
--color-success-500: oklch(0.5 0.15 172);
--color-success-600: oklch(0.44 0.14 172);
--color-success-700: oklch(0.38 0.12 172);
--color-success-800: oklch(0.3 0.1 172);
--color-success-900: oklch(0.22 0.08 172);
--color-success-950: oklch(0.14 0.05 172);
--color-warning-50: oklch(0.95 0.05 60);
--color-warning-100: oklch(0.9 0.08 60);
--color-warning-200: oklch(0.82 0.1 60);
--color-warning-300: oklch(0.74 0.12 60);
--color-warning-400: oklch(0.66 0.14 60);
--color-warning-500: oklch(0.75 0.15 60);
--color-warning-600: oklch(0.65 0.15 60);
/* 0.59 mirrors foundation.css: light-mode press fill under the dark
text-on-warning — hue 60 is the binding case (0.58 = 4.46:1, under AA). */
--color-warning-700: oklch(0.59 0.13 60);
--color-warning-800: oklch(0.45 0.11 60);
--color-warning-900: oklch(0.25 0.08 60);
--color-warning-950: oklch(0.15 0.05 60);
}
/*
* Earthy shadow tint — a faint deep green-grey so shadows sit naturally on the
* stone chassis. :root (not @theme) to mirror semantic.css; imported after the
* base styles, so it wins.
*/
:root {
--blocks-shadow-tint: 0.2 0.025 150;
/* Neutral intent (bg-neutral / text-neutral / borders) follows the stone
chassis instead of the library-default cool grey. */
--neutral-chrome-hue: 150;
}
Global Component Defaults
When CSS token overrides are not enough, use BlocksProvider to set default slotClasses for every component type. Wrap your app once, and every Button, Card, Input etc. picks up the defaults.
Instance-level slotClasses still override the global ones.
Global defaults via BlocksProvider
<script>
import { BlocksProvider } from '@urbicon-ui/blocks';
</script>
<BlocksProvider
defaults={{
Button: {
slotClasses: { base: 'rounded-full font-bold uppercase tracking-wide' }
},
Card: {
slotClasses: { base: 'rounded-3xl' }
},
Input: {
slotClasses: { base: 'rounded-full' }
}
}}
>
<slot />
</BlocksProvider>Defaults sit near the bottom of the precedence chain above — instance slotClasses and class still win. For
prop-conditional defaults (overrides) and named presets, see the BlocksProvider API.
Global Unstyled Mode
For a completely custom design, set unstyled on BlocksProvider. All components strip their default styles and only render the HTML structure.
Use slotClasses (globally
via defaults or per instance) to apply your own design.
Global unstyled mode
<script>
import { BlocksProvider } from '@urbicon-ui/blocks';
</script>
<BlocksProvider unstyled>
<!-- All components render without default styles -->
<slot />
</BlocksProvider>Deep Dives
CSS Token Themes
All built-in themes with live preview and usage instructions.
BlocksProvider API
Global unstyled mode, component defaults, and merge behavior.
Theme Builder
Interactive OKLCH color palette generator with live preview.
Design Tokens
Foundation, semantic, and interaction token reference.
Tier System
The three-tier semantic radius vocabulary — commit / modify / contain — with cascade and override demos.
Color Rooms
How this docs site is themed — the per-section room accent, the --docs-* token catalogue, light/dark via light-dark(), activation and override
recipes.