This document defines the visual language of Modulate product interfaces for web. Its core is a combination of tokens and components, and the system itself is UI-library-agnostic. The HTML/CSS implementation shipped here is a reference — directly usable in a vanilla stack. The Next.js implementation lives in Storybook.
| Layer | What it is | Developer’s rule |
|---|---|---|
Component |
A styled UI element, static or interactive. | Preserve component structure and naming, implement in your own stack. |
Token |
A named CSS variable storing a single property value. | Use tokens as-is: don’t rename, don’t alias, don’t generalize. |
Beyond this, the design system covers color, layout, page composition, graphics, and animation.
This is the source of truth for color values in the palette. If a color in the design file differs slightly, consider using the value from here. If it differs significantly, it may be worth checking with the designer.
text
text-link
bg-hover
text-hover
bg-accent
code-accent
code
text-caption
ui-error
ui-success
ui-border
text-inverted
bg-surface
ui-control
background
black-const
chart-default
chart-model-1
chart-model-2
chart-model-3
chart-status-success
chart-status-server-error
chart-status-client-error
chart-status-processing
emotion-neutral-group
neutral, unknown
emotion-attack-rejection-group
angry, contemptuous, disgusted
emotion-threat-uncertainty-group
afraid, anxious, stressed, surprised, ashamed, frustrated
emotion-calm-grounded-group
calm, confident, interested
emotion-excited-engaged-group
affectionate, amused, excited, happy, hopeful, proud, relieved, curious
emotion-low-energy-negative-group
sad, disappointed, bored, tired, concerned, confused
Each variable stores a full color value, for example:
rgb(253, 47, 75). To apply transparency, or to blend a
color toward the background, use color-mix() directly in
selector properties — this keeps the token list short and makes the
intent explicit in the code.
color: var(--m__accent-color);
color: color-mix(in srgb, var(--m__accent-color) 50%, transparent);
color: color-mix(in srgb, var(--m__accent-color) 50%, var(--m__background-color));
The main unit is
rem, it is used for text sizes and spacing.
:root currently sets font-size: 125%, which
gives 1rem = 20px. This can be tuned more precisely later,
including with separate values for desktop and mobile if needed.
The system supports two layout modes: desktop and mobile. Media queries look at viewport width rather than device type.
Pages are composed of stacked full-width sections,
Navigation is an exception. It sits outside the section flow and is not counted as part of it.
Section content is either a Widget or Text-content. A widget is an interactive element or a dedicated component for presenting data. It's normally presented as a card with rounded corners.
Text-content is free-form HTML with shared styles applied.
A section may contain: one or several Widgets, or one Text-content block (which always spans the full width).
| Component | Sample | Tokens |
|---|---|---|
.m__widget |
--m__widget-radius--m__widget-padding
|
|
| Text-content |
This is for documentation-style copy with
links, inline
|
Components carry a default bottom margin (--m__default-margin-bottom) and no top margin. This keeps vertical stacking predictable and makes
it easy to top-align elements placed side by side.
Spacing between sections or before headings is handled with explicit
spacer elements (.m__space), not margins on content
elements. The idea behind this is that content elements can’t know what
spacing is appropriate in every context — that’s why the spacer should
be placed by whoever assembles the layout.
In this repository, the supported text elements (headings, paragraphs,
links, lists, inline code) are styled through their HTML tags. In
production, especially when combining different versions or styling
systems, that approach can let styles leak inward or
Common responses include:
The same elements show up inside interactive components and inside widgets, and on their own as unstructured HTML in Text-content sections.
| Component | Sample | Tokens |
|---|---|---|
p |
High-performance API that transforms spoken audio into sharp, actionable insights: detecting fraud, surfacing social toxicity, and spotlighting positive human behavior with impressive speed and precision. |
|
strong |
High-performance API that transforms spoken audio into sharp, actionable insights: detecting fraud, surfacing social toxicity, and spotlighting positive human behavior with impressive speed and precision. |
|
a |
License: Proprietary |
|
code |
Authenticate via the |
--m__font-size-mono-m--m__font-mono--m__font-weight-regular--m__code-color
|
h1.loud |
Documentation |
--m__font-gothic--m__font-size-display
|
h1 |
Modulate Platform API (0.1.0) |
|
h2 |
List all jobs associated with the account |
|
ul |
|
|
.caption |
Supported formats: MP3, WAV, OGG, FLAC. Maximum file size 500 MB. |
--m__font-size-s--m__text-caption-color
|
.m__code-block |
|
--m__bg-surface-color--m__radius-m--m__font-mono--m__font-size-mono-m
|
| Component | Sample | Tokens |
|---|---|---|
.m__button |
--m__button-padding--m__radius-m--m__space-xxs--m__button-icon-size--m__bg-accent-color--m__text-hover-color--m__text-inverted-color
|
|
.m__button-secondary |
--m__button-padding--m__radius-m--m__space-xxs--m__button-icon-size--m__text-color--m__text-hover-color
|
|
.m__button-outline |
--m__button-padding--m__radius-m--m__space-xxs--m__button-icon-size--m__text-color--m__text-hover-color
|
|
.m__button-compact |
--m__control-compact-padding--m__radius-s--m__bg-accent-color--m__text-hover-color--m__text-inverted-color
|
|
.m__button-secondary-compact |
Sign in |
--m__control-compact-padding--m__radius-s--m__text-color--m__text-hover-color
|
.m__button-outline-compact |
Sign in |
--m__control-compact-padding--m__radius-s--m__text-color--m__text-hover-color
|
.m__button-preset |
|
--m__button-preset-padding--m__radius-s--m__text-color--m__text-hover-color--m__bg-hover-color
|
.m__button-danger |
--m__button-padding--m__radius-m--m__ui-error-color--m__ui-error-hover-color
|
|
.m__button-danger-compact |
--m__control-compact-padding--m__radius-s--m__ui-error-color--m__ui-error-hover-color
|
|
.m__button-success |
--m__button-padding--m__radius-m--m__ui-success-color--m__ui-success-hover-color
|
|
.m__button-success-compact |
--m__control-compact-padding--m__radius-s--m__ui-success-color--m__ui-success-hover-color
|
|
.m__option-list |
--m__radius-m--m__button-padding--m__space-xs--m__space-xxs--m__ui-border-color--m__ui-control-color--m__text-color
|
|
.m__tag |
Admin Internal |
--m__button-compact-padding--m__radius-xs--m__font-size-s--m__ui-border-color--m__text-color
|
.m__tag-flat |
default |
--m__font-size-xs--m__font-weight-medium--m__radius-s--m__text-caption-color
|
.m__badge |
3 |
--m__font-size-xs--m__font-weight-bold--m__text-color
|
.m__tag-toggle.m__tag-group |
|
--m__button-compact-padding--m__radius-xs--m__font-size-s--m__bg-accent-color--m__bg-hover-color
|
.m__textfield |
|
--m__control-inset-padding--m__radius-s--m__font-size-s--m__ui-control-color--m__ui-border-color--m__text-color--m__text-caption-color--m__text-hover-color--m__bg-hover-color--m__ui-error-color
|
.m__select |
|
--m__control-inset-padding--m__radius-s--m__ui-control-color--m__ui-border-color--m__text-color--m__text-caption-color--m__text-hover-color--m__line-height-base--m__bg-surface-color
|
.m__select-compact |
--m__button-compact-padding--m__radius-s--m__ui-control-color--m__ui-border-color--m__text-color--m__text-caption-color--m__line-height-base--m__bg-surface-color
|
|
.m__datepicker |
|
--m__control-inset-padding--m__radius-s--m__ui-control-color--m__ui-border-color--m__text-color--m__text-caption-color--m__text-hover-color--m__line-height-base--m__bg-hover-color
|
.m__datepicker-compact |
--m__input-padding--m__radius-s--m__ui-control-color--m__ui-border-color--m__text-color--m__text-caption-color--m__text-hover-color--m__bg-hover-color
|
|
.m__segmented-control |
--m__segmented-control-padding--m__radius-s--m__text-color--m__text-hover-color--m__text-link-color
|
|
.m__segmented-control-compact |
--m__segmented-control-padding--m__radius-s--m__text-color--m__text-hover-color
|
|
.m__alert-error.m__alert-success
|
This reset link is invalid or has expired.
Password reset successfully.
|
--m__control-inset-padding--m__radius-m--m__ui-error-color--m__ui-success-color
|
.m__checkbox
|
--m__ui-border-color--m__radius-xs--m__bg-accent-color--m__bg-hover-color--m__background-color--m__text-color--m__text-hover-color--m__unhover-transition-duration--m__transition-easing
|
|
.m__toggle
|
--m__ui-border-color--m__bg-accent-color--m__bg-hover-color--m__background-color--m__text-color--m__text-hover-color--m__unhover-transition-duration--m__transition-easing
|
|
.m__loader |
--m__loader-size
|
|
.m__tooltip |
Customer Service Call |
--m__background-color--m__ui-border-color--m__radius-s--m__font-size-xs--m__text-color
|
.m__popover |
|
--m__background-color--m__ui-border-color--m__radius-m--m__font-size-s--m__text-caption-color--m__text-link-color--m__text-hover-color
|
Icons are collected into one hidden SVG sprite and then reused through
<use>. This keeps each icon defined once, avoids
duplicated SVG markup, and makes color inheritance and updates easier to
manage.
Here are the current icons and their ids:
UI icons — used for interactive controls:
Brand icons and selected signal icons (for example
#deepfake and #emotions) are colored — they
render in fixed colors and do not respond to color or
fill on the parent element.
Charts use Chart.js with a set of opinionated defaults that align them with the design system’s visual language.
The following adjustments are applied on top of the default Chart.js configuration:
plugins.tooltip.enabled: false
animation: false at the
options level.
categoryPercentage: 1,
barPercentage: 0.92 — set at the
options level.
Chart-specific palette tokens use the --m__chart-* prefix.
Grid and tick colors are read from
--m__text-color and
--m__text-caption-color via
getComputedStyle. Grid color is derived as
color-mix(in srgb, --m__text-color 12%, transparent). Palette tokens are defined in
tokens/colors.css.
A MutationObserver watches the
class attribute on <body>.
position: "bottom",
labels.usePointStyle: true,
labels.pointStyle: "circle",
labels.boxWidth: 8,
labels.boxHeight: 8.
plugins.legend.display: false for
single-dataset charts.
ticks.maxRotation: 0
The formatter tracks the current month while iterating through
sorted dates. On month change (or at index 0) it outputs
toLocaleDateString("en-US",
{ month: "short", day: "numeric" }); otherwise — d.getDate().
A CanvasGradient runs from
chartArea.top to
chartArea.bottom with stops at
0 → rgba(…, 0.35),
0.67 → rgba(…, 0.35),
1 → rgba(…, 0). Base color:
--m__chart-default-color. Y-axis
uses grace: "25%".
pointHoverRadius: 2.5 with
pointRadius: 0 on the dataset; hover color
is rgbToRgba(baseRgb, 0.9).
interaction: { mode: "index", intersect: false }
on all charts. Bar hover color uses
hoverBackgroundColor: lightenColor(color, 0.35)
— mixes the base color with white to brighten consistently in
both light and dark themes.
A <p class="chart-status-bar"> element sits
below the chart container; its innerHTML is
replaced on mousemove and cleared on
mouseleave. Values are read with
chart.getElementsAtEventForMode(e, "index", { intersect: false }).
min-height: 1.3em reserves the space so the
widget height does not change.
A <div class="chart-legend"> element is
populated with innerHTML on every render and
updated on mousemove /
mouseleave. Chart.js built-in legend is disabled
(plugins.legend.display: false).
Animation tokens define timing only. The system provides tokens for instant hover, animated unhover, and shared easing, but it does not prescribe which CSS properties should be included in a transition.
| Interaction | Sample | Tokens |
|---|---|---|
hover |
--m__hover-transition-duration--m__unhover-transition-duration--m__transition-easing
|