Styling
How className merging, tv() slots and the two mergers fit together.
cn() for every caller-supplied className
Uniwind does not deduplicate conflicting utilities on its own. A caller's className has to go
through cn() or it will not beat the component's own classes.
import { cn } from "@delacour/native-ui/lib/cn";
<View className={cn(variants.root(), className)} />;tv comes from lib/tv, never from tailwind-variants
There are two mergers in the library and both need the semantic size tokens: cn() merges a
caller's className, and tv() merges slots and variants through a tailwind-merge instance of its
own.
A bare tv from tailwind-variants does not know what button-md is. It drops text-button-md
into tailwind-merge's text colour group, and silently strips the label's colour.
import { tv } from "@delacour/native-ui/lib/tv";src/styles/tokens.ts holds the one config both mergers are built from.
Where a tv() lives
A component with more than one styled part puts one slotted tv() in a *.variants.ts
sibling. That sibling is what makes it possible: the slot set is read by files that cannot import
each other's roots without closing a cycle, so it needs a leaf of its own. It is also where that
component's pure resolvers live, which is what makes them reachable from bun test.
A component that is a single styled element with no pure resolvers declares its tv() above
the component in its own file. Separator is the one that qualifies today.
Pressable holds no tv() at all — its values are opacity and scale interpolation targets read
by a worklet on the UI thread, not styles, and a worklet cannot compile a className.
Overriding sizes
Use size-*, not w-* with h-*. tailwind-merge conflicts size into w/h but not the
reverse, so a trailing w-6 will not clear a leading size-5.
<Icon icon={IconHeart} className="size-8" />What does not work
group-*, peer-* and :has() do not exist. Uniwind's compiler reads data-* off a single
flat selector and its runtime matches them against props on the component carrying the class.
No class on a parent can reach a child. This is why Field cascades its state through a React
context instead of a parent-scoped selector — see Field.
Safe-area utilities resolve to zero. pt-safe and friends compile to
env(safe-area-inset-top), which is nothing on React Native. The class applies, nothing moves, and
a navbar draws over the status bar with no error anywhere. Use useSafeAreaInsets().
A runtime template class is never compiled. Tailwind's scanner is static, so
`size-[${n}px]` has nothing in Uniwind's store to look up and silently draws nothing. Named
sizes are classes; the numeric escape hatch stays a prop or an inline style.
A variant may not name a line height. tailwind-merge lists leading among font-size's
conflicting groups, so a text-lg from a size axis silently deletes a leading-6 written beside
a text-base. Every step in tokens.css already carries a paired
--text-*--line-height; prose needing its own gets a pair, registered in tokens.ts.