Recipes
Give each tenant its own theme
Apply a brand colour from data to one part of a page, with text colours that stay readable.
View as Markdown for coding assistants, or see llms.txt.
Compute a theme from the tenant's settings and put its variables on the element that should be themed. Everything inside follows, and nothing outside changes.
Kauri Timber
New
"use client";
import { Button } from "@azmr/ui/button";
import { Badge, Card, CardContent, CardHeader, CardTitle } from "@azmr/ui/display";
import { createTheme } from "@azmr/ui/theme";
import { ToggleGroup, ToggleGroupItem } from "@azmr/ui/toggle";
import { useState } from "react";
// Each tenant is one brand colour. createTheme turns it into every variable the components read,
// including text colours that stay readable on it, so a tenant cannot pick an unreadable brand.
const TENANTS = {
kauri: { label: "Kauri Timber", primary: "#2E7D32" },
harbour: { label: "Harbour Freight", primary: "#1E88E5" },
sunrise: { label: "Sunrise Bakery", primary: "#FFEB3B" },
} as const;
type TenantId = keyof typeof TENANTS;
export default function TenantTheme() {
const [tenant, setTenant] = useState<TenantId>("kauri");
// Compute the theme from the tenant's setting, then put its variables on the element that
// should be themed. Everything inside follows, and nothing outside changes.
const theme = createTheme({ primary: TENANTS[tenant].primary });
return (
<div className="flex max-w-md flex-col gap-4">
<ToggleGroup
aria-label="Tenant"
variant="outline"
value={[tenant]}
onValueChange={(value) => {
const chosen = (value as string[])[0] as TenantId | undefined;
if (chosen) setTenant(chosen);
}}
>
{(Object.keys(TENANTS) as TenantId[]).map((id) => (
<ToggleGroupItem key={id} value={id}>
{TENANTS[id].label}
</ToggleGroupItem>
))}
</ToggleGroup>
<div style={theme.cssVariables}>
<Card>
<CardHeader>
<CardTitle>{TENANTS[tenant].label}</CardTitle>
</CardHeader>
<CardContent className="flex items-center gap-3">
<Button>Place order</Button>
<Badge>New</Badge>
</CardContent>
</Card>
</div>
</div>
);
}The pattern
import { createTheme } from "@azmr/ui/theme";
const kauri = createTheme({ primary: "#2E7D32" }).cssVariables["--primary"];
const harbour = createTheme({ primary: "#1E88E5" }).cssVariables["--primary"];
const differs = kauri !== harbour;
// => trueThen apply the variables:
<div style={createTheme({ primary: tenant.primary }).cssVariables}>…</div>createThemedoes not return your colour as given. It adjusts it until the text on it, and the focus ring, meet their contrast targets in light and dark mode, so a tenant who picks a hard-to-read brand colour still gets a readable interface. The demo's third tenant is a bright yellow and gets dark text on its buttons. See what it guarantees.- It throws an error that names the option when a value is not a valid colour, so validate what a tenant types before it reaches the page, and show them the message.
- Put the variables on the element, not only on
<html>: a scope that sets its owndata-themere-declares every colour, so variables set outside it do not reach inside. See Apply the theme. - For the whole app, apply the theme to
<html>, and do it on the server so the first paint is already right.