AZMARA PLATFORM
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.

Preview theme
Preview density

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;
// => true

Then apply the variables:

<div style={createTheme({ primary: tenant.primary }).cssVariables}>…</div>
  • createTheme does 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 own data-theme re-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.

On this page