Skip to content

Theming

Simurgh separates raw heritage colors from semantic UI roles. Components consume semantic tokens, so applications can change the visual theme without editing component source or redefining the meaning of a palette color.

Package consumers can import tokens without any component recipes:

@import '@simurgh-ui/styles/tokens.css';

Each component stylesheet also includes the shared tokens. An application that imports only @simurgh-ui/styles/button.css does not need a second token import. CLI consumers should load the copied src/styles/simurgh/tokens.css file from the application’s global stylesheet entry.

Load CSS in this order:

  1. The application reset or framework base layer.
  2. Simurgh tokens and component recipes.
  3. Application token, recipe, and utility overrides.
@import './reset.css';
@import '@simurgh-ui/styles/dialog.css';
@import './theme.css';
@import './component-overrides.css';

Simurgh does not ship a global reset and does not normalize every native element. The consuming application remains responsible for global box-sizing, typography inheritance, body margin, and other reset policy. Inspect native controls when switching between resets because browser defaults can remain visible on components with deliberately minimal recipes.

Simurgh styles do not currently declare CSS cascade layers. Their normal selectors participate in the unlayered author cascade. If an application uses layers, place the imports into an explicit project layer and keep application overrides in a later layer:

@layer reset, vendor, app;
@import './reset.css' layer(reset);
@import '@simurgh-ui/styles/dialog.css' layer(vendor);
@layer app {
.simurgh-dialog {
padding: 1.5rem;
}
}

Use source order and intentional layers before increasing selector specificity or adding !important. Component recipes generally use single classes and documented state attributes so an application rule in the same layer and later source order can override them predictably.

Choose one of these patterns per CSS entry:

  • Import only the component stylesheets in use; each one includes tokens.css.
  • Import tokens.css alone for fully headless components or application code that consumes tokens without recipes.
  • Import all.css once when the application deliberately needs the complete recipe catalog.

Do not import tokens.css, recipes.css, all.css, and component stylesheets together by default. That repeats declarations and makes source order harder to understand. Build tools may deduplicate identical imports, but the application should not depend on tool-specific deduplication for correct cascade behavior.

Tailwind is optional; Simurgh requires no Tailwind plugin or preset. Put Simurgh CSS in the project’s vendor/component layer and application utilities in the intended later layer. Tailwind versions and project configurations differ in how generated utilities are layered, so confirm the built CSS order rather than assuming a utility always wins.

Use semantic tokens for application-wide brand decisions and utilities for local layout or composition. When a utility repeatedly fights a recipe selector, prefer a documented recipe or token override instead of escalating specificity at every call site.

Color values are HSL channels rather than complete color functions. Use them as hsl(var(--simurgh-primary)), including an optional alpha such as hsl(var(--simurgh-primary) / 0.8).

TokenLight defaultDark defaultRole and contrast expectation
--simurgh-background39 55% 97%222 47% 9%Application canvas. Pair with foreground.
--simurgh-foregroundvar(--simurgh-ink)39 55% 92%Primary text and icons. Must remain readable on background and surface.
--simurgh-surface40 43% 99%222 40% 13%Raised controls, cards, menus, and dialogs. Pair with foreground.
--simurgh-muted39 31% 91%220 30% 20%Subtle selected, hover, and secondary surfaces.
--simurgh-muted-foreground217 20% 38%39 18% 70%Secondary text. Must remain readable on background, surface, and muted.
--simurgh-primaryvar(--simurgh-persian-blue)176 57% 47%Primary actions and emphasized states. Pair with primary-foreground.
--simurgh-primary-foreground0 0% 100%222 47% 9%Text and icons placed on primary.
--simurgh-accentvar(--simurgh-saffron)42 88% 62%Accent decoration or emphasis. Do not assume it is a text background without testing the chosen foreground.
--simurgh-dangervar(--simurgh-pomegranate)Inherits light valueDestructive actions and error emphasis. Pairings require contrast testing in both themes.
--simurgh-border32 28% 78%219 29% 28%Control and surface boundaries. Must remain perceivable next to adjacent surfaces.
--simurgh-ringvar(--simurgh-firuzeh)176 57% 53%Keyboard focus indicator. Must contrast with every surface on which focus can appear.
--simurgh-radius0.625remInherits light valueShared corner radius for recipe surfaces and controls.
--simurgh-shadow0 14px 40px rgb(15 23 42 / 0.16)Inherits light valueElevation shadow for floating and raised surfaces.
--simurgh-duration160msInherits light valueShared transition duration; becomes 1ms under reduced-motion preference.

Additional semantic roles keep component recipes and application utilities from overloading the core surface colors:

Token familyRoles
Actionssecondary, secondary-foreground, danger-foreground
Statussuccess, success-foreground, warning, warning-foreground, information, information-foreground
Controlsinput-surface, input-border, disabled-surface, disabled-foreground, hover, pressed
Layersscrim, shadow-sm, shadow, shadow-lg
Sizingcontrol-sm (32px), control-md (40px), control-lg (48px), space-1 through space-4
Typographyfont-sans, text-sm, text-md, text-lg, line-height

Foreground roles are mandatory partners for their named backgrounds. Status colors communicate emphasis, not meaning by themselves; retain text or icon labels. Control sizes are minimum block sizes and may grow for wrapped or localized content.

“Inherits light value” means the dark selector does not override that token; normal CSS inheritance keeps the :root value.

Changing a semantic pair transfers responsibility for its contrast to the application. At minimum, test foreground on background and surface, muted-foreground on its supported surfaces, primary-foreground on primary, focus ring against every focusable surface, and border against both sides of the boundary. Test light, dark, forced-colors, disabled, hover, focus, and selected states rather than checking isolated swatches.

Choose the least invasive level that meets the design requirement. The levels can be combined, but each one transfers more styling responsibility to the application.

Use tokens for brand colors, corner shape, elevation, and motion while retaining the shipped component recipes:

@import '@simurgh-ui/styles/dialog.css';
:root {
--simurgh-primary: 265 72% 44%;
--simurgh-ring: 265 82% 55%;
--simurgh-radius: 1rem;
}

This is the preferred application-wide customization level. Behavior, DOM, recipe selectors, and state presentation remain owned by Simurgh.

2. Override recipe classes and state hooks

Section titled “2. Override recipe classes and state hooks”

Load the component recipe, then add application rules after it. Dialog recipes expose classes such as .simurgh-overlay, .simurgh-content, and .simurgh-dialog; interactive components also expose documented ARIA and data-* state attributes.

@import '@simurgh-ui/styles/dialog.css';
.simurgh-dialog {
border-block-start: 0.25rem solid hsl(var(--simurgh-accent));
padding: 1.5rem;
}
.simurgh-overlay[data-state='open'] {
backdrop-filter: blur(0.25rem);
}

Only target classes, attributes, and DOM parts documented on the component page. Internal element order and undocumented selectors are not a stable styling contract during the pre-release period.

Omit the component recipe when the application will provide every visual state. Keep tokens only if the application still wants the shared semantic system:

import {
Dialog,
DialogContent,
DialogOverlay,
DialogPortal,
DialogTrigger,
} from '@simurgh-ui/react/dialog';
import '@simurgh-ui/styles/tokens.css';
import './account-dialog.css';
export function AccountDialog() {
return (
<Dialog>
<DialogTrigger className="account-dialog-trigger">
Edit account
</DialogTrigger>
<DialogPortal>
<DialogOverlay className="account-dialog-overlay" />
<DialogContent
className="account-dialog-content"
aria-label="Edit account"
>
Account fields
</DialogContent>
</DialogPortal>
</Dialog>
);
}

Headless styling removes only the optional visual recipe; component behavior and accessibility logic still run. The application must style default, hover, focus-visible, active, open/closed, disabled, invalid, loading, reduced-motion, forced-colors, dark, and RTL states that apply. CLI consumers may also edit the copied markup and classes because that source is application-owned.

Declare application overrides after importing Simurgh styles:

:root {
--simurgh-primary: var(--simurgh-persian-blue);
--simurgh-accent: var(--simurgh-saffron);
--simurgh-ring: var(--simurgh-firuzeh);
--simurgh-radius: 0.375rem;
}

Prefer semantic overrides to changing raw heritage tokens. A raw token such as --simurgh-persian-blue describes a color; --simurgh-primary describes how the interface uses a color.

Dark tokens activate when .dark or data-theme="dark" appears on the component or any ancestor:

<html data-theme="dark">
<!-- application -->
</html>

Override dark roles under the same selector after the Simurgh import:

.dark,
[data-theme='dark'] {
--simurgh-background: 225 32% 8%;
--simurgh-surface: 225 28% 12%;
--simurgh-foreground: 40 35% 94%;
}

The downloadable light/dark theme overrides every semantic color, shape, shadow, and motion token and preserves the reduced-motion override. Load it after a Simurgh component recipe:

@import '@simurgh-ui/styles/dialog.css';
@import '/examples/custom-theme.css';

The documentation build validates the semantic token surface and its documented theme behavior. It also checks 20 WCAG contrast pairings: normal text pairs at a minimum of 4.5:1 and focus-ring and border boundaries at a minimum of 3:1. These automated checks cover the declared pairings, not every component state; keyboard focus, forced colors, disabled states, and application overrides still require browser testing.

The shipped palette exposes firuzeh, persian-blue, lapis, cobalt, brick, ochre, saffron, pomegranate, ivory, and ink under the --simurgh-* prefix. These are source colors for semantic mapping, not guaranteed foreground/background pairs.

The palette is informed by Iranian turquoise and cobalt tile glazing, lapis and ultramarine decoration, ochre pigments and brick, and strong blue/yellow and red/white contrasts. Semantic mappings are adjusted where necessary to retain readable interface contrast.

--simurgh-duration becomes 1ms under prefers-reduced-motion: reduce. Preserve that behavior in custom animation rules and avoid introducing essential information through motion alone.

Recipes use logical CSS properties so dir="rtl" does not require a separate stylesheet. Use logical properties such as padding-inline, margin-inline-start, and inset-inline-end in application overrides to preserve that behavior.

Simurgh does not impose a universal size or variant prop. Native attributes and behavioral props belong to the adapter API; visual choices belong to application classes, stable parts, state attributes, and semantic tokens. This keeps copied and headless components open to an application’s own design vocabulary without promising a variant API that a component does not implement.

Scope density on an ancestor instead of repeating overrides on every child. The shipped modes use logical dimensions and affect interactive controls rather than content surfaces:

ModeControl targetMenu/item targetIntended use
comfortable44px40pxTouch-friendly forms, dialogs, and primary application flows
compact40px36pxDefault mixed-input desktop and responsive interfaces
dense32px28pxData-heavy expert tools with an alternative spacious/touch mode
<form data-density="comfortable">...</form>
<div class="data-grid-tools" data-density="dense">...</div>

compact matches the default 40px control rhythm. Dense mode remains above WCAG’s 24px minimum target size, but proximity and motor-access needs still matter; do not make it the only mode for a touch-first product. Override --simurgh-control-height, --simurgh-control-padding, and --simurgh-item-height when product research supports a different scoped policy.

Content-sized surfaces such as Dialog, Popover, Sheet, Sidebar, and Tooltip already use viewport relative maximum sizes. Override their documented recipe class with min(), max(), clamp(), and logical dimensions rather than relying on internal wrappers.

Create application variants with a class or a project-owned data-variant attribute. Forward it to the same root element identified by the component page’s attribute-forwarding and styling-contract sections:

<Button className="billing-action" data-variant="danger">
Delete payment method
</Button>
.billing-action[data-variant='danger'] {
background: hsl(var(--simurgh-danger));
color: white;
}

Do not reuse behavioral attributes such as data-state, aria-expanded, or aria-invalid as a visual variant switch. They describe component state and must retain their documented meaning.

Icons are consumer content; Simurgh does not require an icon package. Decorative icons need aria-hidden="true", while an icon-only control needs an accessible name. Use currentColor, a logical gap, and a fixed flex size so the icon follows disabled, danger, dark, and high-contrast foreground styles:

<Button className="button-with-icon">
<TrashIcon aria-hidden="true" focusable="false" />
Delete
</Button>
<Button aria-label="Close dialog" className="icon-button">
<CloseIcon aria-hidden="true" focusable="false" />
</Button>
.button-with-icon {
display: inline-flex;
gap: 0.5rem;
align-items: center;
}
.button-with-icon > svg,
.icon-button > svg {
inline-size: 1em;
block-size: 1em;
flex: none;
}
.icon-button {
inline-size: 2.5rem;
padding-inline: 0;
}

Prefer container queries when a component responds to its placement and media queries when the whole viewport changes the interaction. Preserve DOM order, keyboard order, labels, and component state across breakpoints; CSS may rearrange presentation, but it should not create a second hidden interactive copy.

.profile-card {
container-type: inline-size;
}
.profile-card__actions {
display: flex;
gap: 0.75rem;
}
@container (inline-size < 24rem) {
.profile-card__actions {
flex-direction: column;
}
.profile-card__actions > * {
inline-size: 100%;
}
}

For overlays, test narrow and short viewports, browser zoom, virtual keyboards, and long translated labels. Use 100dvh only with a suitable fallback when older target browsers are supported.

Build optional transitions from --simurgh-duration and the component’s documented state hooks. Reduced motion should remove spatial movement, repeated animation, parallax, and long fades rather than merely making them slightly faster:

.simurgh-dialog {
transition:
opacity var(--simurgh-duration) ease,
transform var(--simurgh-duration) ease;
}
.simurgh-dialog[data-state='closed'] {
opacity: 0;
transform: translateY(0.5rem);
}
@media (prefers-reduced-motion: reduce) {
.simurgh-dialog {
transition: none;
}
.simurgh-dialog[data-state='closed'] {
transform: none;
}
}

Never delay focus movement, form feedback, or dismissal until an animation finishes. When replacing recipe CSS, retain state semantics even if no animation is rendered.

Use .dark or [data-theme='dark'] for theme activation and dir="rtl" on the document or the nearest independently directed subtree. Prefer semantic-token overrides for dark mode; avoid per-component color inversions that bypass contrast pairs. Prefer logical properties and directional icons. Mirror arrows that communicate previous/next or physical direction, but do not mirror logos, media playback icons, checkmarks, or text-direction-independent symbols.

.next-icon {
transform: rotate(0deg);
}
[dir='rtl'] .next-icon {
transform: rotate(180deg);
}
ConcernMost relevant componentsRecommended surface
Size and densityButtons, fields, menus, toolbars, pagination, tagsRoot class plus project-owned density ancestor
Visual variantsActions, badges, alerts, cards, feedback statesProject-owned class or data-variant
IconsButtons, menu items, fields, tree items, navigationConsumer content using currentColor and accessible naming
Responsive layoutDialog, Sheet, Drawer, Sidebar, tables, forms, toolbarsLogical dimensions, container/media queries, stable DOM order
AnimationDialog, Drawer, Sheet, Popover, Tooltip, Toast, disclosure componentsDocumented data-state or ARIA hooks and --simurgh-duration
Dark modeEvery visible recipeSemantic tokens under .dark or [data-theme='dark']
RTLEvery directional layout; especially Sheet, Dialog, Tree, navigationdir, logical CSS properties, and selectively mirrored icons

Test customized components in their default, hover, focus-visible, active, disabled, invalid, loading, open/closed, light/dark, LTR/RTL, reduced-motion, forced-colors, zoomed, and narrow-viewport states where those states apply.

Last verified on 2026-08-13 against Simurgh registry 0.1.1.