How Tokens Work
Every token is a CSS custom property declared on :root inside
_content/Marai.UI/marai-ui.min.css. Components reference these tokens
rather than hardcoded values. Changing a token changes every component that uses it.
Color tokens use bare HSL channels so they can be composed with an hsl() wrapper and
support alpha variants without duplication:
/* The token stores channels, not a full color value. */
--marai-primary: 221.2 83.2% 53.3%;
/* Components consume it like this: */
background-color: hsl(var(--marai-primary));
/* Alpha variants work without an extra token: */
background-color: hsl(var(--marai-primary) / 0.15);Token Reference
Surface
Background and text colors for surfaces — the foundation of every layout.
| Token | Default (light) | Purpose |
|---|---|---|
--marai-background | White | Page background |
--marai-foreground | Near-black | Default text |
--marai-card | White | Card surface |
--marai-card-foreground | Near-black | Card text |
--marai-popover | White | Popover / dropdown surface |
--marai-popover-foreground | Near-black | Popover text |
--marai-muted | Slate-100 | Subtle fills and disabled surfaces |
--marai-muted-foreground | Slate-500 | Secondary and placeholder text |
--marai-accent | Slate-100 | Hover fills for ghost/outline variants |
--marai-accent-foreground | Slate-900 | Text on accent fills |
Action
Intent-driven colors for interactive elements.
| Token | Purpose |
|---|---|
--marai-primary | Primary action (default brand blue) |
--marai-primary-foreground | Text on primary background |
--marai-secondary | Secondary action |
--marai-secondary-foreground | Text on secondary background |
--marai-destructive | Destructive / danger action |
--marai-destructive-foreground | Text on destructive background |
Feedback
Semantic status colors for toasts, alerts, and badges.
| Token | Purpose |
|---|---|
--marai-success / --marai-success-foreground | Positive confirmation |
--marai-warning / --marai-warning-foreground | Cautionary state |
--marai-info / --marai-info-foreground | Informational state |
Border / Input
| Token | Purpose |
|---|---|
--marai-border | Default border color across all components |
--marai-input | Input field border color |
--marai-ring | Focus ring color |
Shape
| Token | Default | Purpose |
|---|---|---|
--marai-radius | 0.5rem | Global border radius for all components |
--marai-shadow-sm | Subtle lift | Cards, inputs |
--marai-shadow | Moderate lift | Elevated surfaces |
--marai-shadow-lg | Strong lift | Dialogs, toasts, overlays |
Typography
| Token | Default |
|---|---|
--marai-font-size-xs | 0.75rem |
--marai-font-size-sm | 0.875rem |
--marai-font-size-base | 1rem |
--marai-font-size-lg | 1.125rem |
--marai-font-size-xl | 1.25rem |
--marai-font-size-2xl | 1.5rem |
--marai-line-height-tight | 1.25 |
--marai-line-height-normal | 1.5 |
--marai-line-height-relaxed | 1.75 |
--marai-font-weight-normal | 400 |
--marai-font-weight-medium | 500 |
--marai-font-weight-semibold | 600 |
--marai-font-weight-bold | 700 |
Animation
| Token | Default | Purpose |
|---|---|---|
--marai-duration-fast | 100ms | Hover, focus transitions |
--marai-duration-base | 150ms | Standard UI transitions |
--marai-duration-slow | 300ms | Toasts, dialogs entering |
--marai-ease | cubic-bezier(0.4, 0, 0.2, 1) | Default easing curve |
Overriding Tokens
Override any token in your own CSS file — no component edits required. Place overrides
after the Marai.UI stylesheet in your App.razor or equivalent host file.
Change the primary brand color
:root {
/* Use bare HSL channels — no hsl() wrapper here. */
--marai-primary: 265 85% 55%;
--marai-primary-foreground: 0 0% 100%;
}Make corners sharper globally
:root {
--marai-radius: 0.25rem;
}Slow down all motion
:root {
--marai-duration-fast: 150ms;
--marai-duration-base: 250ms;
--marai-duration-slow: 450ms;
}Dark Mode
Dark mode activates by adding the dark class to the root element (html
or body). All tokens redefine automatically — no per-component work needed.
<html class="dark">...</html>
Marai.UI's ThemeService handles this for you. Call InitializeAsync()
once in your root layout's OnAfterRenderAsync to load the stored preference and
apply the correct class on page load. Use ToggleAsync() to flip between modes,
or SetDarkMode(bool) to set a specific mode programmatically.
@inject ThemeService Theme
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await Theme.InitializeAsync();
}
// Toggle between light and dark:
await Theme.ToggleAsync();
// Set a specific mode programmatically:
await Theme.SetDarkMode(true); // force dark
await Theme.SetDarkMode(false); // force lightThemeService automatically persists the preference to localStorage and
detects the system preference (prefers-color-scheme) if no preference is stored.
data-state Hook Reference
Components expose data-state attributes so you can drive visual changes entirely
from CSS or Tailwind arbitrary-data-attribute utilities — no JavaScript state wiring required.
Use data-[state=value]:* in the Class parameter, or target the attribute
in your own CSS.
| Component | Element | Attribute | Values |
|---|---|---|---|
MTabsTrigger |
trigger <button> |
data-state |
"active" / "inactive" |
MSwitch |
root <label>, track <span>, thumb <span> |
data-state |
"checked" / "unchecked" |
MSwitch |
root <label> |
data-disabled |
"true" / "false" |
MAccordionItem |
item wrapper <div> |
data-state |
"open" / "closed" |
MAccordionContent |
content <div> |
data-state |
"open" / "closed" |
MDropdownContent |
content <div> |
data-state |
"open" / "closed" |
Example — change the accordion trigger's chevron color when open:
<MAccordionTrigger Class="data-[state=open]:text-primary">
Section title
</MAccordionTrigger>Creating a Custom Brand Theme
A brand theme is a single CSS block that overrides tokens for both light and dark mode. No component code changes — only token overrides.
/* fintech-theme.css */
:root {
/* Brand primary: deep indigo */
--marai-primary: 243 75% 55%;
--marai-primary-foreground: 0 0% 100%;
/* Neutral surface */
--marai-background: 220 20% 98%;
--marai-card: 0 0% 100%;
--marai-border: 220 14% 90%;
/* Tighter radius for a more structured feel */
--marai-radius: 0.375rem;
}
.dark {
--marai-primary: 243 85% 65%;
--marai-primary-foreground: 243 60% 10%;
--marai-background: 224 25% 8%;
--marai-card: 224 22% 11%;
--marai-border: 224 18% 20%;
}Add the theme CSS after the Marai.UI stylesheet and before your Tailwind stylesheet so token overrides cascade correctly.
How Semantic Colors Map to Components
Components never reference a specific color value — they reference a semantic token. This
means swapping --marai-primary globally changes buttons, badges, focus rings,
and any other element that uses the primary color.
| Component | Tokens consumed |
|---|---|
| MButton (Primary) | --marai-primary, --marai-primary-foreground |
| MButton (Outline / Ghost) | --marai-accent, --marai-accent-foreground, --marai-input |
| MCard | --marai-card, --marai-card-foreground, --marai-border, --marai-shadow-sm |
| MInput | --marai-input, --marai-ring, --marai-muted-foreground |
| MSeparator | --marai-border |
| MToast (success) | --marai-success, --marai-success-foreground |
| Focus rings | --marai-ring |