Theming guide
Brand the kit for a brokerage without breaking meaning or accessibility.
Two layers
- Tokens (
@hetibank/theme) — a typedThemeTokensobject: colors, typography, radius,
spacing, shadows, brand identity.
- CSS variables — the only styling interface components read. You can set them directly and
skip the React theme entirely.
:root {
--hb-primary: #5b3a29;
--hb-primary-fg: #ffffff;
--hb-accent: #c2703d;
--hb-accent-fg: #1a0d06;
--hb-bg: #faf8f6;
--hb-card: #ffffff;
--hb-radius-card: 0.75rem;
--hb-font-sans: "Inter", system-ui, sans-serif;
}
createBrandTheme
import { createBrandTheme } from "@hetibank/theme";
const theme = createBrandTheme({
brand: {
name: "Desert Bloom Realty",
primary: "#5b3a29", // branded surfaces, primary buttons
accent: "#c2703d", // marks, rules, emphasis
logoUrl: "/brand/desert-bloom.svg",
markUrl: "/brand/desert-bloom-mark.svg",
},
mode: "light", // or "dark"
});
The returned theme embeds a contrast report. Creation throws when:
- body text on the page background is below 4.5:1
- card text is below 4.5:1
- button foreground on primary/accent is below 4.5:1
- the brand mark is below 3:1
- any chart color is below 3:1 against the background
The canonical audit found two real brokerage accents failing this (eXp #F58220 at 2.59:1, Realty ONE #A58C58 at 3.23:1 for text). Those hex values are kept as fixtures in tools/verify/contrast.mjs proving the gate still rejects them.
If you must ship a failing brand during a design review, pass allowInaccessible: true — the theme then carries contrast.ok === false and the reason, so it cannot silently reach production.
Dark mode
Render both themes and switch with an attribute:
<ThemeProvider theme={isDark ? darkTheme : lightTheme} as="section">
ThemeProvider sets data-hb-theme="light|dark" and the full variable set inline, so a page can have multiple independently themed regions (e.g. a dark assistant panel in a light site). The generated @hetibank/theme/styles.css also supports a global data-hb-theme="dark" selector.
What may change, what may not
| May change | May never change |
|---|---|
| logo, mark, name | --hb-danger / --hb-warning / --hb-success meanings |
| primary/accent color, type, radius, density | certainty labels or evidence text |
| chart palette (contrast-checked) | permissions, actions or calculations |
| light/dark mode | figure values or units |
A branded theme that changes what a number means is a defect: theme is presentation only.
Branding a sub-tree
<ThemeProvider theme={brokerageTheme} as="section" className="hb-brand-region">
<SearchResults />
</ThemeProvider>
The nested provider overrides variables for that subtree. Use it for a white-label funnel embedded in a partner site.
Brokerage-first identity, required attribution
Consumer branding leads with the brokerage. MLS/licensing attribution stays visible in the footer:
<ShellFooter attribution="Listing data © Las Vegas REALTORS®. HETIBANK figures are estimates." />
Do not hide attribution to make a theme look cleaner; it is a licensing requirement.
Typography and density
--hb-text-basesets the base size (default 16px).--hb-leadingsets line-height (1.5).--hb-spaceis the 4px spacing unit the components multiply.--hb-radius-*controls control/card/chip rounding.- Changing density is supported; changing font family is supported. Do not introduce a display
font that fails contrast for small labels.
Testing a theme
node tools/verify/contrast.mjs
Or in code:
import { checkContrast, auditThemeColors, contrastRatio } from "@hetibank/theme";
const ratio = contrastRatio("#c2703d", "#faf8f6"); // 3.48
checkContrast("brand-mark", "#c2703d", "#faf8f6", "largeText").pass; // true
Run the theme at 360px, 768px and 1280px with long copy. The kit's states are contrast-checked, but layout is your composition.