Customization
Style override layers, slot-based styling, and data-slot DOM contracts.
Moraine components expose typed slots and four class override layers. CSS theme tokens are a separate choice: change a semantic color or radius in CSS to update every component using that token. With UnoCSS, presetMoraine() emits neutral defaults and accepts named color overrides; with Tailwind, define them in CSS. Use defineTheme() when you need to change component recipes or default variants.
The 4-Layer Override Hierarchy#
When styling a component, choose the layer matching your intended scope:
| Layer | Surface | Scope | How to configure |
|---|---|---|---|
| 1 | Base Recipe | Library defaults | Built-in styles shipped with Moraine. |
| 2 | Theme Layer | Application or subtree defaults | Wrap in <MoraineProvider theme={defineTheme(...)}> |
| 3 | Instance Slots | Named slots on one instance | classes={{ slot: '...' }} / styles={{ slot: { ... } }} |
| 4 | Direct Element | Target DOM element on a part | class="..." / style={{ ... }} directly on the element |
How Classes Merge Across Layers#
Classes merge in ascending priority (Layer 1 → Layer 2 → Layer 3 → Layer 4) using Moraine’s cn merger. Conflicting Tailwind/UnoCSS classes (e.g. bg-primary vs bg-destructive or p-2 vs p-4) resolve with the higher layer winning:
See Composition for public parts vs style slots, and Theming for app-wide defaults (Layer 2).
Style Slots & the data-slot Contract#
CamelCase in TSX Props#
In TypeScript JSX, slot names on classes and styles are always camelCase:
Kebab-case data-slot in DOM#
In the rendered DOM, Moraine tags elements with standardized kebab-case data-slot attributes:
- Root elements:
data-slot="<component>"(e.g.data-slot="select",data-slot="button"). - Child slots:
data-slot="<component>-<slot>"(e.g.data-slot="select-control",data-slot="select-item-label").
This contract enables clean global CSS selectors or testing queries:
TypeScript Slot Types#
Each component namespace exports typed Classes, Styles, and Slot shapes: