use Margo Theme

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

API

useMargoTheme

The whole theme API is this one hook. It returns the current theme and the two ways to change it, and keeps the value in sync by observing the class attribute of the document element.

proptypedefault
themeMargoTheme | undefinedthe theme currently applied to the document; undefined during server render and hydration
set(next: MargoTheme) => voidapplies a theme
toggle() => voidswitches to the other one
tsx
import { margoTheme, useMargoTheme } from "margo-ui";

const { theme, set, toggle } = useMargoTheme();

set(margoTheme.DARK);
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

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.

The hook takes no default: the default theme is the class you write on the html element in the server render, and a missing dark class always reads as light, which is what the CSS renders. Changing the theme writes exactly one of the two classes.

tsx
<html className={margoTheme.DARK}>

On the server

theme is undefined during server render and hydration, then the value found on the document element. Handle that case, or drive theme-dependent UI with the dark: variant, which needs no JavaScript and cannot flash.

set and toggle write to the document, so call them from event handlers or effects, never while rendering. That is also where a persisted preference belongs: persistence is deliberately left out of the kit, because where it lives — storage, cookie, user account — is a decision only the application can make.

tsx
const { set } = useMargoTheme();

useEffect(() => {
  const stored = localStorage.getItem("theme");

  set(stored === margoTheme.LIGHT ? margoTheme.LIGHT : margoTheme.DARK);
}, [set]);

Why the hook needs no provider

tsx
const { theme, toggle } = useMargoTheme();
const isLight = theme === margoTheme.LIGHT;

<Button onClickBlur={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.