margoThemeClient

Reads, sets and toggles the theme. The state is the .dark class on the document element — where the CSS already looks for it — and nothing mirrors it.

API

API

proptypedefault
get() => MargoThemethe theme currently applied to the document
set(next: MargoTheme) => voidapplies a theme
toggle() => voidswitches to the other one
tsx
import { margoTheme, margoThemeClient } from "margo-ui";

margoThemeClient.get();                  // "light" | "dark"
margoThemeClient.set(margoTheme.DARK);
margoThemeClient.toggle();

margoTheme

The two theme names as constants, so a comparison is checked by the compiler instead of spelled out as a string at every call site.

proptypedefault
margoTheme.LIGHT"light"the light theme
margoTheme.DARK"dark"the dark theme

useMargoTheme

The React side. It returns the current theme and a setter, in the shape of a state hook, and keeps the value in sync by observing the class attribute of the document element with a mutation observer.

proptypedefault
[0]MargoThemethe current theme, kept in sync with the document
[1](next: MargoTheme | ((current) => MargoTheme)) => voidsets it, with the updater form of a state setter

Details

MetaColorScheme

A one line component that declares the document supports both schemes. It is what makes the browser paint its own surfaces — scrollbars, form controls, the address bar on mobile — to match the theme instead of assuming light.

tsx
<head>
  <MetaColorScheme />
</head>

Why the DOM holds the state

The CSS already needs the theme as a class on the document: that is how the dark variables win. Keeping a second copy in React would mean two sources of truth that can disagree, and a first paint that flashes the wrong one. Reading the class back is the cheaper and more honest option.

On the server

Both APIs are safe to call during server rendering: without a document the getter reports dark and the setter does nothing. That makes dark the assumed default of the first paint, so an app that persists a light preference should apply it as early as possible.

tsx
const stored = localStorage.getItem("theme");

margoThemeClient.set(stored === margoTheme.LIGHT ? margoTheme.LIGHT : margoTheme.DARK);

Persistence is deliberately left out of the kit: where the preference lives — storage, cookie, user account — is a decision only the application can make, and the client is small enough to drive from anywhere.

Why the hook needs no provider

tsx
const [theme, setTheme] = useMargoTheme();
const isLight = theme === margoTheme.LIGHT;

<Button onClickBlur={() => margoThemeClient.toggle()} aria-pressed={isLight}>
  <Button.Label label={isLight ? "Light" : "Dark"} />
</Button>

Because the subscription is to the DOM and not to a store, two toggles rendered in different parts of the tree stay in sync without a provider between them — and so does a theme changed from outside React entirely.