Foundations
Styling and overrides
class, classes, and the one merge rule that catches everyone.
Two props cover every override. class styles the root. classes reaches every other element by name. Nothing else is needed, and nothing is out of reach.
class wins
class is merged last, through cn(), so your utility always beats the component's own.
Class wins
class lands on the root and is merged last, so rounded-none beats the pill.
<script lang="ts"> import { Button } from 'omaris';</script><Button>Default</Button><Button class="rounded-none">Squared off</Button><Button class="h-14 px-10 text-base">Bigger than any size prop</Button> That is the whole rule for single-element components — Button, Chip, Badge, Switch and the rest render one element, so class is the entire story there.
classes reaches the parts
A component that renders more than one element declares each as a named part. Pass Tailwind for one of them and nothing else moves.
Classes parts
Anything deeper than the root is a named key on classes.
<script lang="ts"> import { Input } from 'omaris'; let value = $state('42.0000');</script><Input class="w-72 max-w-full" label="Reading" supportingText="Monospace digits, italic help" bind:value classes={{ control: 'font-mono tabular-nums', support: 'italic' }}/> Every page in this reference lists the parts its component has, under Parts. The keys are typed and exported too — InputClasses, TableClasses — so your editor lists them.
<Table classes={{ headCell: 'uppercase tracking-wide', row: 'even:bg-surface-container-low' }}/> class is sugar for the root part, and still beats classes.root.
The merge rule that catches people
Classes merge through tailwind-merge, which only resolves conflicts within a group. h-64 replaces a built-in h-56, because both are heights. But aspect-video does not cancel a height at all — different group, so both survive and the height wins.
Where a component ships a value that would be caught by this, it gives you a prop and a custom property instead of a class, so a class genuinely can replace it.
The merge trap
tailwind-merge only resolves conflicts inside one group, so aspect-video cannot cancel a height. Where a component ships one it gives you a prop.
height="auto" + an aspect class
<script lang="ts"> import { Carousel } from 'omaris'; const PHOTOS = [ { id: 1, bg: 'bg-primary-container' }, { id: 2, bg: 'bg-tertiary-container' }, { id: 3, bg: 'bg-secondary-container' } ];</script><div class="flex w-full max-w-2xl flex-col gap-2"> <p class="text-body-sm text-muted-foreground">height="auto" + an aspect class</p> <Carousel items={PHOTOS} label="By ratio" height="auto" classes={{ viewport: 'aspect-[16/6]' }}> {#snippet item(photo)} <div class="size-full {photo.bg}"></div> {/snippet} </Carousel></div> Borrowing the variants
Every component exports its tv() object — buttonVariants, cardVariants, inputVariants. Call it to put a component's look on your own element.
Borrowing variants
Every component exports its variants, so your own element can wear them.
<script lang="ts"> import { buttonVariants } from 'omaris';</script><a href="https://svelte.dev" class={buttonVariants({ variant: 'tonal', tone: 'tertiary' })}> A link that looks like a button</a><span class={buttonVariants({ variant: 'outlined', size: 'xs' })}>Not even interactive</span> What not to do
- Don't invent a one-off prop. If a part is hard to reach, it is a missing slot, not a missing
fooClassprop. - Don't use raw values.
bg-primary, notbg-[#6750A4];rounded-shape-lg, notrounded-[16px]. Raw values do not follow the theme, so they break the moment someone changes the hue or turns on high contrast. - Don't reach past the API with a descendant selector.
[&_.some-class]:…binds you to markup that is free to change. Every part has a name; use it.