Modulate Design System

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.

Color

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.

UI Color Palette

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

Charts Color Palette

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 Color Palette

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.

CSS
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));

Layout

Units

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.

Page composition

Pages are composed of stacked full-width sections, not column grids. Each section always spans the full available width and takes as much height as its content needs; sections are self-contained and can be reordered without side effects. Complex layouts live inside sections, not across them.

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
This is for interactive elements and purpose-built widgets that display data.
--m__widget-radius
--m__widget-padding
Text-content

This is for documentation-style copy with links, inline code, headings, and lists, when the content is mostly unstructured HTML rather than a purpose-built UI widget.

Spacing

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.

Components

Text

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

Common responses include:

  • scoping styles at the component boundary, using class-based selectors instead of bare tags where outward leakage is a concern,
  • adding component-level CSS resets where inward leakage is a concern,
  • or another approach that fits the stack.

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 Authorization header using Basic auth.

--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
  • The username should be equal to your account ID.
  • The password should be equal to your API key.
.caption

Supported formats: MP3, WAV, OGG, FLAC. Maximum file size 500 MB.

--m__font-size-s
--m__text-caption-color
.m__code-block
example.yaml
endpoint: /api/velma-2-stt-batch
method: POST
curl -X POST https://api.modulate.ai \
  -H "X-API-Key: your_key"
--m__bg-surface-color
--m__radius-m
--m__font-mono
--m__font-size-mono-m

Interactive

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
Enter a valid email address
--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

Graphics

Icons

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:

#modulate
#account
#api-docs
#api-key
#batch
#behaviors
#billing
#decrease
#done
#calendar
#models
#overview
#prosocial
#streaming
#transcript
#usage
#search
#deepfake
#emotions
#google
#microsoft
#facebook

UI icons — used for interactive controls:

#chevron-down
#chevron-left
#close
#checkmark

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

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:

  • Tooltips are off.

    plugins.tooltip.enabled: false

  • Animation is off.

    animation: false at the options level.

  • One bar / label per time unit. For example, data arriving at sub-day granularity is aggregated by day before rendering.
  • Bars fill the full category width with a small gap between them.

    categoryPercentage: 1, barPercentage: 0.92 — set at the options level.

  • All colors come from CSS tokens.

    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.

  • Theme-aware re-rendering. When the theme class on the body element changes, all charts are destroyed and re-created with fresh colors from CSS variables.

    A MutationObserver watches the class attribute on <body>.

  • Legend at the bottom with round 8×8 markers shown only when there is more than one dataset.

    position: "bottom", labels.usePointStyle: true, labels.pointStyle: "circle", labels.boxWidth: 8, labels.boxHeight: 8.

    plugins.legend.display: false for single-dataset charts.

  • X-axis labels stay horizontal. At narrow widths labels are auto-skipped rather than rotated.

    ticks.maxRotation: 0

  • Smart X-axis labels. The first label on the axis and the first label of each new month include the abbreviated month name; all other labels show only the day number.

    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().

  • Gradient fill when the axis does not start at zero. The filled area fades to transparent in the bottom 1/3.

    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%".

  • Hover interaction. Moving the cursor over a chart activates a vertical index zone spanning the full chart height — the active position corresponds to a single day, not to individual shapes or points. On line charts a dot appears at the data point; on bar charts the hovered column is brightened.

    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.

  • Status bar on line charts. While hovering, a reserved single line below the chart shows the date and value for that position. The line is empty when the cursor is outside the chart area.

    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.

  • Merged legend and status bar on bar charts. The legend below the chart doubles as a hover status bar: by default it shows colored dots with dataset labels; while hovering it transforms to show the date followed by each label with its colored value.

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

Animations

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