Install
One package. It pulls @75neo/themes in with it, so the styles arrive at the same time as the components. Needs React 18 or newer.
Swap pnpm add for npm install, yarn add or bun add as you prefer.
Import the theme
The library styles itself with Tailwind CSS, so both imports go in the stylesheet your app already loads.
@import "tailwindcss";
@import "@75neo/themes";
That one import ships the tokens, the light and dark variants, and the base layer.
Dark mode is a .dark class on a root element and nothing else needs wiring, so a
theme toggle only has to move that class.
Render something
import { Button } from "@75neo/react";
export function App() {
return (
<Button variant="solid" color="primary">
Get started
</Button>
);
}
<script setup lang="ts">
import { Button } from "@75neo/vue";
</script>
<template>
<Button variant="solid" color="primary">Get started</Button>
</template>
Both adapters render the same markup, carry the same data-slot attributes, and take
the same props. Anything below is true of both unless it says otherwise.
The cascade
Four layers can set a component’s classes, and they fold weakest first. Tailwind-merge settles conflicts, so a later layer replaces a utility it conflicts with and every non-conflicting utility from every layer survives.
- The recipe. What the component looks like out of the box.
Themelayers. EveryThemeabove the component, already folded into one config.- The
uiprop. This call site, per slot. classorclassName. This call site, reaching thebaseslot only.
That is the whole model. The three ways to restyle below are entry points into it.
Redefine the tokens
The broadest change, and the one a rebrand usually wants. Every value is a plain custom property, so redefining it after the import moves everything built on it.
:root {
--ui-radius: 0.5rem;
--ui-primary: var(--color-teal-700);
}
.dark {
--ui-primary: var(--color-teal-400);
}
Each semantic color is a single token. Recipes spend it at different strengths with
Tailwind’s opacity modifier — bg-primary/10 for a soft fill, hover:bg-primary/75 for
a solid hover — rather than reaching for a second token. That is what keeps a rebrand
down to one line per color, with no role left to retune by hand.
Wrap a subtree in Theme
Restyles every component below it. Nesting composes: an inner Theme is folded onto the
one it sits inside, so a component reads one flat config rather than walking a chain.
import { Theme, Button } from "@75neo/react";
<Theme theme={{ button: { ui: { base: "rounded-full" }, props: { color: "neutral" } } }}>
<Button>Rounded and neutral by default</Button>
</Theme>;
<script setup lang="ts">
import { Theme, Button } from "@75neo/vue";
</script>
<template>
<Theme :theme="{ button: { ui: { base: 'rounded-full' }, props: { color: 'neutral' } } }">
<Button>Rounded and neutral by default</Button>
</Theme>
</template>
ui adds classes per slot. props sets new defaults for the component’s variant props,
which a call site can still override.
Keep the config object stable. A fresh literal on every render re-folds the theme chain for everything underneath it.
Pass ui to one component
The narrowest change, and the one that reads best where a call site is genuinely a one-off.
<Button ui={{ base: "rounded-full", label: "tracking-wide" }}>One-off</Button>
<Button :ui="{ base: 'rounded-full', label: 'tracking-wide' }">One-off</Button>
The keys are the component’s slots, which is the same word as its data-slot attribute
and the same word the recipe uses. One vocabulary per component, and the reference lists
it.
Type safety
The theme registry is a global interface that each component module augments, so a
ThemeConfig only accepts keys for components that exist, and only accepts props those
components actually take.
import type { ThemeConfig } from "@75neo/react";
const theme: ThemeConfig = {
button: { props: { color: "neutral" } },
// buton: {} — a typo here is a type error, not a silent no-op
};
<script setup lang="ts">
import type { ThemeConfig } from "@75neo/vue";
const theme: ThemeConfig = {
button: { props: { color: "neutral" } },
// buton: {} — a typo here is a type error, not a silent no-op
};
</script>
A theme is plain data with no functions and no class instances, so it stays serializable and safe to reuse across trees.