M
Marai.UI
v1.0.0-alpha.7
Marai.UI is built on a semantic CSS variable system. Every visual decision — color, radius, shadow, typography, and motion — flows through design tokens you can override without touching a single component.

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:

css
/* 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.

TokenDefault (light)Purpose
--marai-backgroundWhitePage background
--marai-foregroundNear-blackDefault text
--marai-cardWhiteCard surface
--marai-card-foregroundNear-blackCard text
--marai-popoverWhitePopover / dropdown surface
--marai-popover-foregroundNear-blackPopover text
--marai-mutedSlate-100Subtle fills and disabled surfaces
--marai-muted-foregroundSlate-500Secondary and placeholder text
--marai-accentSlate-100Hover fills for ghost/outline variants
--marai-accent-foregroundSlate-900Text on accent fills

Action

Intent-driven colors for interactive elements.

TokenPurpose
--marai-primaryPrimary action (default brand blue)
--marai-primary-foregroundText on primary background
--marai-secondarySecondary action
--marai-secondary-foregroundText on secondary background
--marai-destructiveDestructive / danger action
--marai-destructive-foregroundText on destructive background

Feedback

Semantic status colors for toasts, alerts, and badges.

TokenPurpose
--marai-success / --marai-success-foregroundPositive confirmation
--marai-warning / --marai-warning-foregroundCautionary state
--marai-info / --marai-info-foregroundInformational state

Border / Input

TokenPurpose
--marai-borderDefault border color across all components
--marai-inputInput field border color
--marai-ringFocus ring color

Shape

TokenDefaultPurpose
--marai-radius0.5remGlobal border radius for all components
--marai-shadow-smSubtle liftCards, inputs
--marai-shadowModerate liftElevated surfaces
--marai-shadow-lgStrong liftDialogs, toasts, overlays

Typography

TokenDefault
--marai-font-size-xs0.75rem
--marai-font-size-sm0.875rem
--marai-font-size-base1rem
--marai-font-size-lg1.125rem
--marai-font-size-xl1.25rem
--marai-font-size-2xl1.5rem
--marai-line-height-tight1.25
--marai-line-height-normal1.5
--marai-line-height-relaxed1.75
--marai-font-weight-normal400
--marai-font-weight-medium500
--marai-font-weight-semibold600
--marai-font-weight-bold700

Animation

TokenDefaultPurpose
--marai-duration-fast100msHover, focus transitions
--marai-duration-base150msStandard UI transitions
--marai-duration-slow300msToasts, dialogs entering
--marai-easecubic-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

css
:root {
    /* Use bare HSL channels — no hsl() wrapper here. */
    --marai-primary: 265 85% 55%;
    --marai-primary-foreground: 0 0% 100%;
}

Make corners sharper globally

css
:root {
    --marai-radius: 0.25rem;
}

Slow down all motion

css
: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
<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.

csharp
@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 light

ThemeService 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:

razor
<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.

css
/* 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.

ComponentTokens 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