Skip to content
omaris

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.

Monospace digits, italic help
<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.

A link that looks like a button Not even interactive
<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 fooClass prop.
  • Don't use raw values. bg-primary, not bg-[#6750A4]; rounded-shape-lg, not rounded-[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.