Using Tailwind CSS

Tailwind v4 works with ReactUnity. Its utilities are ordinary CSS, and what they are built out of — cascade layers, @property, oklch(), rem and unitless line heights — is read here. What does not carry over is Preflight, Tailwind’s reset for a document, so @reactunity/renderer ships one written for what ReactUnity renders instead.

Setting it up

Install Tailwind and its Vite plugin:

npm install -D tailwindcss @tailwindcss/vite

Add the plugin to the app’s Vite config, before ReactUnity’s own:

import reactUnity from '@reactunity/renderer/vite';
import tailwind from '@tailwindcss/vite';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [tailwind(), reactUnity()] });

Then write the entry stylesheet, and import it once from the app’s entry file rather than per page:

@import "tailwindcss";

That is the line Tailwind’s own docs give you, and it is all you need: ReactUnity’s Vite plugin aliases the bare tailwindcss import to @reactunity/renderer/tailwind.css, which is everything @import "tailwindcss" brings except that Preflight is ReactUnity’s. Subpaths are left alone, so tailwindcss/theme.css and the rest still resolve to the package.

Pass tailwind: false to reactUnity() to turn the alias off. Tailwind’s own Preflight then applies, and it lands unevenly: every rule in it that names html fails to match, so the root font family and line height it means to set never apply, while * still strips the padding ReactUnity’s controls carry. Without the plugin at all — or with the alias off — spell it out instead:

@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@import "@reactunity/renderer/tailwind.css";

The first three lines are Tailwind’s own recipe for leaving Preflight out. The last one is what goes in its place, and comes last so its theme values win.

What the ReactUnity Preflight does

  • Clears every margin and padding, which here means the padding on a button and an input and the margin around a toggle, so a p-* or m-* utility is the only thing that decides
  • Removes the grey fill and the rounded corners those controls come with, leaving that to bg-* and rounded-*. appearance is left alone — it is what makes them behave as controls
  • Sets line-height: 1.5 and the shipped sans-serif family on the root
  • Gives a placeholder half of whatever colour it inherits, in place of the opacity ReactUnity dims it with, which a colour utility cannot undo

Either half can be imported alone. To keep ReactUnity’s control styling and take only the theme:

@import "@reactunity/renderer/tailwind/theme.css";

To take only the reset, @import "@reactunity/renderer/tailwind/preflight.css" layer(base);. The layer matters: an unlayered rule beats every utility, so a preflight outside one would undo them.

Taking the reset and then writing a control look of your own back into the same layer is the third option, and what the kitchen-sink sample does — its entry stylesheet ends with a @layer base block giving button, input and toggle their padding and fill again:

@import "tailwindcss";

@layer base {
  button {
    @apply rounded-lg bg-neutral-200 pt-2 pr-3 pb-2 pl-3;
  }
}

Reach the theme through @apply rather than by writing var(--color-neutral-200) yourself. A theme variable is only emitted where a utility that was generated needs it, and @apply is what counts as generating one — a var() written by hand resolves to nothing unless some page happens to use the matching utility.

The theme

Nearly all of Tailwind’s default theme carries over untouched — the spacing scale and radii in rem, the type scale with its unitless line heights, the colours in oklch(), the shadows, the transition timings. Two values are replaced, because their defaults are stacks of web font names that resolve to nothing here: --font-sans and --font-mono become sans-serif and monospace, the two families ReactUnity ships. See Using custom fonts to add more, and name them in a @theme block of your own after the import.

--font-serif is left as it is. ReactUnity ships no serif font, and naming one it has not got would be worse than a font-serif utility that does nothing.

flex does not mean a row

Every element in ReactUnity is a flex container already, and the default direction is column, not row — which is closer to how a document stacks its blocks than to how CSS flexbox starts out. So flex on its own changes nothing, and flex-row is the utility that turns the axis around.

Dark mode

Tailwind’s dark: variant is @media (prefers-color-scheme: dark), and nothing in ReactUnity answers that query — there is no engine setting to read it from. The class strategy works fully:

@custom-variant dark (&:where(.dark, .dark *));

dark: then follows a dark class on any ancestor. The other way is to answer the query yourself, from anywhere that can reach the context, after which the default variant starts working:

context.MediaProvider.SetValue("prefers-color-scheme", "dark");

What does not work

UtilitiesWhy
grid, grid-cols-*, col-span-*Layout is flexbox only. The flex utilities are the way
@container, @sm:Container queries are not implemented
has-* variants:has() is not implemented
line-clamp-*, contents, mix-blend-*The properties behind them are not implemented

Arbitrary values (p-[13px], leading-[1.7]), arbitrary properties, @theme, @utility, @custom-variant and the hover:, active:, focus: and disabled: variants all work.

The utilities v4 writes as logical properties work too, px-*, py-*, mx-*, my-*, inset-x-*, border-x-*, ps-*, ms-*, start-* and space-x-* among them. They follow direction, which is inherited — see logical properties.