MobTranslate styleguide

Color schemes, tones and customization

Choose a source and scheme deliberately, inspect realized HCT and role relationships, and verify scoped changes before adopting them.

Source, palette, role and application

Generation does not decide which task is important or establish review authority. Record source, algorithm/version, scheme choices, role mappings and the actual surfaces/states that were verified. Keep a captured scheme distinguishable from a draft setting and a deployed product theme.

LayerResponsibilityFailure
Source colorA deliberate input to generationTreating a wallpaper or photograph as automatically permitted product identity
Key colorsStarting colors for related palette familiesAssuming every family reproduces the original seed
Tonal palettesA range of realizable colors at chosen tonesCalling an HSL lightness ramp an HCT palette
Semantic rolesPurpose, pairings, theme and contrast relationshipsUsing one attractive swatch for every foreground/state
ApplicationMap the agreed roles to actual components and surfacesDeclaring a generated palette a complete accessible theme

Hue, chroma, tone and gamut

HCT combines hue, chroma and perceptual tone to support related colors and contrast-aware roles. Tone is based on CIE lightness; it is not interchangeable with the lightness slider in HSL. A request for high chroma at a very light or dark tone may exceed the available sRGB gamut.

Report what was requested and what the algorithm actually produced. Hue can wrap around its circular domain, and near-neutral colors do not preserve the same chromatic identity as a saturated request. Keep the realized hex/HCT values alongside the request instead of silently calling both exact.

CoordinateTool contractInspection
Hue0–360° requestCompare realized hue and circular wrap
Chroma0–150 bounded experimentInspect actual gamut-limited chroma
Tone0–100 requestInspect realized lightness and pair contrast
OutputOpaque sRGB #RRGGBBNo alpha, image composition or wide-gamut claim
typescript
import {Hct, hexFromArgb} from "@material/material-color-utilities";
const requested = {hue: 49, chroma: 80, tone: 95};
const realized = Hct.from(requested.hue, requested.chroma, requested.tone);
const hex = hexFromArgb(realized.toInt());
// Inspect realized.hue/chroma/tone; requested chroma may not be achieved.

Static, brand and dynamic scheme selection

The shared MobTranslate theme is an authored Earth palette, not a generated Material baseline. This explorer produces local proposals without replacing those tokens. A manual hex/HCT choice is an explicit source input; the browser cannot automatically read a person’s wallpaper through this tool.

Use stable source selection and fallbacks when content changes or a platform cannot provide a color. A source-derived accent should not change permission, error or review meaning. Content attribution and cultural use permissions remain separate from pixel selection.

ChoiceUse whenReview
Static baselineA stable authored scheme fits the product and tasksVerify role/state pairs in both themes
Custom brandAn established identity needs deliberate palette/role choicesKeep legibility and role relationships ahead of exact brand shade reproduction
User-generated sourcePeople explicitly choose personalization or a supported platform supplies itKeep preference ownership, fallback and task meaning clear
Content-based sourceRelevant, permitted content can guide a contextual accentVerify the actual pixels, selection algorithm and scope
Multiple sources/schemesDifferent contextual regions need a documented relationshipKeep coherent role pairs inside each scope and avoid conflicting primary emphasis

Working source-to-role explorer

Capture a proposal and inspect the SDK’s role collection, declared backgrounds/contrast targets, tonal key colors, sampled tones and full JSON. Draft edits do not rewrite the captured output. Requested/effective specification versions expose SDK fallback rather than presenting unsupported variant parity.

Tonal spot, Expressive and Fidelity are actual SDK constructors. This tool pins Material Color Utilities 0.4.0, requests the selected 2021/2025 specification and uses its phone algorithm parameter. Those choices are captured; they are not device detection, OS personalization or native-theme installation.

The original SVG fixture has four equal-area bands. Extraction decodes its actual image, quantizes pixels and asks the SDK to rank a suitable seed. It does not merely choose the largest area, and it does not classify the image’s cultural meaning. The result populates the draft only.

Generate a captured proposal with Material Color Utilities 0.4.0. The algorithm’s platform parameter is phone; it does not detect your device or read a wallpaper. Preview theme is independent of this page’s light/dark theme.

Original four-band color fixture: ochre, green, blue and warm white in equal areas

Original vector fixture: 64×64 pixels. Band specification: 16×64 #b45e2a; 16×64 #386b4f; 16×64 #395d75; 16×64 #faf8f5. It is not a source photograph or cultural asset.

Draft source and scheme

Hex colors must use #RRGGBB. Requested hue is 0–360°, chroma 0–150 and tone 0–100. The sRGB gamut can reduce realized chroma; HCT is not HSL. Custom-color harmonization does not rewrite the generated semantic roles.

Draft settings are valid; capture to apply them to the local proposal.

Initial proposal. Shared Earth tokens are unchanged.

Captured variant/theme/contrast
tonal; light; 0
Requested → effective specification
2025 → 2025
Realized source
#b45e2a
Realized hue/chroma/tone
46.817 / 47.387 / 49.345
Captured custom color
#386b4f → #386b4f; harmonization disabled
Available role values
49, derived from the SDK’s current allColors collection

SDK fallback is explicit. Some variants resolve a requested 2025 scheme through 2021 rules. A specification/variant name is not a conformance or contrast certificate.

Scoped action specimen

Only this surface and action use the captured role pairs. Hover/pressed keep the same fill here; a full state system needs additional design and verification.

primary: #895131; realized tone 40.287

SDK background classification: background. SDK contrast-curve target: 4.5.

  • surface_dim (#ecd5cb): 4.550:1. AA normal-text threshold: passes; 3:1 comparison: passes.
RoleHexDeclared backgrounds and ratios
background#fff8f6No declared pairing
on_background#3d2f28surface_dim: 9.138:1
surface#fff8f6No declared pairing
surface_dim#ecd5cbNo declared pairing
surface_bright#fff8f6No declared pairing
surface_container_lowest#ffffffNo declared pairing
surface_container_low#fff1ebNo declared pairing
surface_container#feeae1No declared pairing
surface_container_high#f9e4dbNo declared pairing
surface_container_highest#f5ded3No declared pairing
on_surface#3d2f28surface_dim: 9.138:1
on_surface_variant#6c5c53surface_dim: 4.538:1
outline#89776esurface_dim: 3.033:1
outline_variant#c3aea4surface_dim: 1.508:1
inverse_surface#130d0aNo declared pairing
inverse_on_surface#a79a95inverse_surface: 7.066:1
primary#895131surface_dim: 4.550:1
primary_dim#7b4526surface_container_high: 6.301:1
on_primary#fff7f5primary: 6.050:1
primary_container#fab089surface_dim: 1.284:1
on_primary_container#602f12primary_container: 6.084:1
primary_fixed#fab089surface_dim: 1.284:1
primary_fixed_dim#eba37dNo declared pairing
on_primary_fixed#461b01primary_fixed_dim: 7.102:1
on_primary_fixed_variant#6b381aprimary_fixed_dim: 4.564:1
inverse_primary#fab089inverse_surface: 10.679:1
secondary#775849surface_dim: 4.562:1
secondary_dim#6a4d3esurface_container_high: 6.249:1
on_secondary#fff7f5secondary: 6.066:1
secondary_container#ffdbcasurface_dim: 1.086:1
on_secondary_container#684b3csecondary_container: 6.104:1
secondary_fixed#ffdbcasurface_dim: 1.086:1
secondary_fixed_dim#f5ccb8No declared pairing
on_secondary_fixed#54392bsecondary_fixed_dim: 7.116:1
on_secondary_fixed_variant#735545secondary_fixed_dim: 4.563:1
tertiary#6e5e26surface_dim: 4.540:1
tertiary_dim#61521bsurface_container_high: 6.279:1
on_tertiary#fff8edtertiary: 6.045:1
tertiary_container#fee7a1surface_dim: 1.148:1
on_tertiary_container#63541dtertiary_container: 6.093:1
tertiary_fixed#fee7a1surface_dim: 1.148:1
tertiary_fixed_dim#efd994No declared pairing
on_tertiary_fixed#50420btertiary_fixed_dim: 7.069:1
on_tertiary_fixed_variant#6e5e26tertiary_fixed_dim: 4.563:1
error#a73b21surface_dim: 4.542:1
error_dim#791903surface_container_high: 8.779:1
on_error#fff7f6error: 6.044:1
error_container#fd795asurface_dim: 1.855:1
on_error_container#6e1400error_container: 4.571:1

Palette key color: #a66846; realized HCT 46.678 / 32.232 / 50.037.

Requested toneRealized hex/tone/chromaSwatch
0#000000; 0.000; 0.000
10#331100; 9.835; 27.844
20#512407; 20.149; 32.006
30#6c391b; 29.896; 32.016
40#895030; 40.030; 32.259
50#a66846; 50.037; 32.232
60#c3815d; 59.973; 31.962
70#e19b75; 69.990; 31.840
80#ffb690; 80.027; 31.122
90#ffdbca; 89.946; 13.691
100#ffffff; 100.000; 2.869
Full captured request, roles, palettes and custom color

Preparing the full-precision captured output.

Role namespace CSS export
css
/* Material role namespace only; not a complete MobTranslate theme. */
.generated-color-proposal {
  --proposal-background: #fff8f6;
  --proposal-on-background: #3d2f28;
  --proposal-surface: #fff8f6;
  --proposal-surface-dim: #ecd5cb;
  --proposal-surface-bright: #fff8f6;
  --proposal-surface-container-lowest: #ffffff;
  --proposal-surface-container-low: #fff1eb;
  --proposal-surface-container: #feeae1;
  --proposal-surface-container-high: #f9e4db;
  --proposal-surface-container-highest: #f5ded3;
  --proposal-on-surface: #3d2f28;
  --proposal-on-surface-variant: #6c5c53;
  --proposal-outline: #89776e;
  --proposal-outline-variant: #c3aea4;
  --proposal-inverse-surface: #130d0a;
  --proposal-inverse-on-surface: #a79a95;
  --proposal-primary: #895131;
  --proposal-primary-dim: #7b4526;
  --proposal-on-primary: #fff7f5;
  --proposal-primary-container: #fab089;
  --proposal-on-primary-container: #602f12;
  --proposal-primary-fixed: #fab089;
  --proposal-primary-fixed-dim: #eba37d;
  --proposal-on-primary-fixed: #461b01;
  --proposal-on-primary-fixed-variant: #6b381a;
  --proposal-inverse-primary: #fab089;
  --proposal-secondary: #775849;
  --proposal-secondary-dim: #6a4d3e;
  --proposal-on-secondary: #fff7f5;
  --proposal-secondary-container: #ffdbca;
  --proposal-on-secondary-container: #684b3c;
  --proposal-secondary-fixed: #ffdbca;
  --proposal-secondary-fixed-dim: #f5ccb8;
  --proposal-on-secondary-fixed: #54392b;
  --proposal-on-secondary-fixed-variant: #735545;
  --proposal-tertiary: #6e5e26;
  --proposal-tertiary-dim: #61521b;
  --proposal-on-tertiary: #fff8ed;
  --proposal-tertiary-container: #fee7a1;
  --proposal-on-tertiary-container: #63541d;
  --proposal-tertiary-fixed: #fee7a1;
  --proposal-tertiary-fixed-dim: #efd994;
  --proposal-on-tertiary-fixed: #50420b;
  --proposal-on-tertiary-fixed-variant: #6e5e26;
  --proposal-error: #a73b21;
  --proposal-error-dim: #791903;
  --proposal-on-error: #fff7f6;
  --proposal-error-container: #fd795a;
  --proposal-on-error-container: #6e1400;
}

All measurements use opaque sRGB role values. They do not cover overlays, every interaction state, color perception or cultural authority. Extraction reads only the original local fixture; wallpaper, browser personal color and remote source records are not read.

Role families and pairing

These families are guidance and generated data, not a claim that every Material role already exists as a MobTranslate token. The explorer’s allColors collection is read from the pinned SDK; background associations are derived from each role’s declarations instead of a hand-maintained synonym/pairing dictionary. Palettes also expose their actual key colors.

Material role familyPurposeLocal treatment
Primary / on-primaryMain accent and readable content on its fillExplorer action specimen maps this pair locally
Secondary / tertiarySupporting accents with their own paired contentAvailable in generated roles/palettes; no automatic mapping to a source status
Containers / on-containerLess dominant accent surfacesKeep the matching pair and contained task relationship
Error / on-errorError emphasis and readable contentA generated error color does not supply an error message
Surface / on-surfaceOrdinary reading and component surfacesExplorer uses this opaque pair for its scoped specimen
Surface containersLow through high containment layersTone-based differentiation is separate from shadow/stacking
Bright / dim surfacesSurface adaptations for the chosen schemeInspect actual theme/contrast output
Inverse surface/content/primaryContrasting contextual treatmentVerify its local background and duration
Outline / outline variantRequired or supporting boundariesDo not treat every outline variant as a required 3:1 control boundary
Fixed accents / on-fixed variantsAccent treatments with defined cross-theme relationshipsInspect the SDK’s actual fixed/dim backgrounds and paired foregrounds
Shadow / scrim / tintLayering and treatment inputsOpaque role values alone do not prove a composited overlay’s contrast

Contrast levels and honest measurements

Contrast level is an algorithm input, not a WCAG certificate. Standard, medium and high can change tone assignments while preserving relationships. The SDK also allows lower-contrast experimentation; it may produce pairs unsuitable for normal text.

Measure realized opaque role values and distinguish the SDK’s curve target from the criterion that applies to the final use. Normal-text AA uses 4.5:1; large text and required non-text information have their own definitions and contexts. A displayed rounded ratio must not be rounded into a pass.

  • The explorer checks both declared backgrounds when a role supplies them. An absent relationship remains absent.
  • The opaque ratio uses realized lightness/relative luminance through the SDK’s contrast utility, with black/white and reference-pair tests.
  • Unsafe normal-text preview pairs suppress the specimen while preserving role values in the neutral interface.
  • Images, gradients, opacity, overlays and state changes need their actual composed background assessed.
  • Keep color-independent names and state information, including failure/recovery messages.

Combine and remap colors deliberately

Keep each contextual scheme in an explicit scope. A brand header and content-derived panel can coexist when their primary emphasis, surfaces, text and focus treatment have coherent boundaries. A portalled surface needs the intended values in its own rendered scope; inheritance from an unrelated trigger is not sufficient.

Remap a role because the element’s purpose warrants it, then inspect its foreground, surrounding surface, state and adjacent controls. Do not choose a different token for every isolated screenshot. Retain an auditable mapping from generated roles to the library/application contract.

The specimen maps only primary/foreground plus an opaque surface and focus treatment. Hover/pressed deliberately retain the same fill. Its CSS export uses a separate proposal namespace; it is not a complete production theme or an automatic migration.

OperationRequired evidence
Local proposalActual computed surface/action colors and unchanged root tokens
Semantic remapPurpose, old/new role and every affected pair/state
Mixed schemesDefined regions and readable boundaries/focus
PortalCorrect container/theme scope and dismissal/focus behavior
AdoptionReviewed mapping, compatibility and verification before replacing shared tokens

New static colors and custom roles

Define a custom color when a real product purpose cannot be represented by the existing contract. Name that purpose and its background/foreground, theme behavior, contrast and state responsibilities. A palette sample or culturally significant source color does not automatically become an interface role.

Keep custom-role definition separate from creating a new baseline or dynamic scheme. Record which algorithm or reviewed fixed values supply it, and test all actual contexts. Do not create a color-only vocabulary for source evidence or permission status.

Baseline adjustment, fidelity and harmonization

The custom-color control implements an explicit SDK harmonization experiment and preserves original/result coordinates. It does not rewrite generated semantic roles, grant rights to use an asset or guarantee contrast. Keep recognizable error/status meaning rather than harmonizing everything for decoration.

AdjustmentMeaningVerify
Own baselineA reviewed stable set of role assignmentsLight/dark/state compatibility and actual consumer output
Own dynamic schemeA deliberate algorithm/palette/role policyVersions, source inputs, fallback and contrast across representative seeds
FidelityA scheme choice intended to retain source character in relevant rolesInspect actual outputs and effective spec; it is not exact seed reproduction everywhere
HarmonizationBlend a custom color’s hue relationship toward a sourceOriginal/result values, role meaning and independent contrast
Direct role overrideReplace an agreed role value for a reasonEvery associated foreground, surface, focus and state

Design-to-development handoff

  • Record the source rationale, original bytes/provenance where applicable, algorithm version, requested/effective scheme and contrast settings.
  • Share complete role definitions and mappings, rather than screenshots or a few hex values alone.
  • Use named semantic pairs in the design file and the actual consuming components; inspect the rendered result after export/import.
  • Make manual/static decisions distinguishable from platform-personalized and content-derived values.
  • Specify a fallback for unavailable sources and a strategy for updating/changing content without unexpected task-state changes.
  • Keep a reviewed migration boundary and verify the consumers before changing shared Earth tokens.

Compiled generation helper

typescript
import {
  argbFromHex,
  hexFromArgb,
  Hct,
  TonalPalette,
  Blend,
  Contrast,
  SchemeTonalSpot,
  SchemeExpressive,
  SchemeFidelity,
  type DynamicColor,
} from '@material/material-color-utilities';

const constructors = {
  tonal: SchemeTonalSpot,
  expressive: SchemeExpressive,
  fidelity: SchemeFidelity,
};
export type ColorVariant = keyof typeof constructors;
export interface ColorProposalInput {
  sourceMode: 'hex' | 'hct';
  sourceHex: string;
  hue: number;
  chroma: number;
  tone: number;
  variant: ColorVariant;
  specVersion: '2021' | '2025';
  isDark: boolean;
  contrastLevel: number;
  customHex: string;
  harmonize: boolean;
}
export const initialColorInput: ColorProposalInput = {
  sourceMode: 'hex',
  sourceHex: '#b45e2a',
  hue: 49,
  chroma: 45,
  tone: 50,
  variant: 'tonal',
  specVersion: '2025',
  isDark: false,
  contrastLevel: 0,
  customHex: '#386b4f',
  harmonize: false,
};
const validHex = (value: string) => /^#[\da-f]{6}$/i.test(value);
export function validateColorInput(input: ColorProposalInput): string[] {
  const errors: string[] = [];
  if (input.sourceMode !== 'hex' && input.sourceMode !== 'hct')
    errors.push('Choose a supported source mode.');
  if (input.sourceMode === 'hex' && !validHex(input.sourceHex))
    errors.push('Source must be an opaque #RRGGBB color.');
  if (input.sourceMode === 'hct') {
    if (!Number.isFinite(input.hue) || input.hue < 0 || input.hue > 360)
      errors.push('Hue must be 0–360 degrees.');
    if (
      !Number.isFinite(input.chroma) ||
      input.chroma < 0 ||
      input.chroma > 150
    )
      errors.push('Chroma must be 0–150.');
    if (!Number.isFinite(input.tone) || input.tone < 0 || input.tone > 100)
      errors.push('Tone must be 0–100.');
  }
  if (!Object.hasOwn(constructors, input.variant))
    errors.push('Choose a supported scheme variant.');
  if (input.specVersion !== '2021' && input.specVersion !== '2025')
    errors.push('Choose a supported specification version.');
  if (
    !Number.isFinite(input.contrastLevel) ||
    input.contrastLevel < -1 ||
    input.contrastLevel > 1
  )
    errors.push('Contrast level must be −1–1.');
  if (!validHex(input.customHex))
    errors.push('Custom color must be an opaque #RRGGBB color.');
  if (typeof input.isDark !== 'boolean' || typeof input.harmonize !== 'boolean')
    errors.push('Theme and harmonization choices must be explicit booleans.');
  return errors;
}
const coordinates = (argb: number) => {
  const color = Hct.fromInt(argb);
  return {
    hex: hexFromArgb(argb),
    hue: color.hue,
    chroma: color.chroma,
    tone: color.tone,
  };
};
export function opaqueColorRatio(first: string, second: string): number {
  if (!validHex(first) || !validHex(second))
    throw new RangeError('Contrast requires two opaque #RRGGBB colors.');
  return Contrast.ratioOfTones(
    Hct.fromInt(argbFromHex(first)).tone,
    Hct.fromInt(argbFromHex(second)).tone,
  );
}
export function buildColorProposal(input: ColorProposalInput) {
  const errors = validateColorInput(input);
  if (errors.length) throw new RangeError(errors.join(' '));
  const source =
    input.sourceMode === 'hex'
      ? Hct.fromInt(argbFromHex(input.sourceHex))
      : Hct.from(input.hue, input.chroma, input.tone);
  const Scheme = constructors[input.variant],
    scheme = new Scheme(
      source,
      input.isDark,
      input.contrastLevel,
      input.specVersion,
      'phone',
    );
  const roles = scheme.colors.allColors.map((color) => {
    const argb = color.getArgb(scheme),
      hex = hexFromArgb(argb);
    const backgrounds = [
      color.background?.(scheme),
      color.secondBackground?.(scheme),
    ].filter(
      (background): background is DynamicColor => background !== undefined,
    );
    const seen = new Set<string>();
    return {
      name: color.name,
      hex,
      tone: Hct.fromInt(argb).tone,
      isBackground: color.isBackground,
      sdkTarget:
        color.contrastCurve?.(scheme)?.get(input.contrastLevel) ?? null,
      backgrounds: backgrounds
        .filter((background) => {
          if (seen.has(background.name)) return false;
          seen.add(background.name);
          return true;
        })
        .map((background) => {
          const backgroundHex = hexFromArgb(background.getArgb(scheme));
          return {
            name: background.name,
            hex: backgroundHex,
            ratio: opaqueColorRatio(hex, backgroundHex),
          };
        }),
    };
  });
  const palettes = Object.entries({
    primary: scheme.primaryPalette,
    secondary: scheme.secondaryPalette,
    tertiary: scheme.tertiaryPalette,
    neutral: scheme.neutralPalette,
    neutralVariant: scheme.neutralVariantPalette,
    error: scheme.errorPalette,
  }).map(([name, palette]) => ({
    name,
    keyColor: coordinates(palette.keyColor.toInt()),
    tones: Array.from({ length: 11 }, (_, index) => ({
      requestedTone: index * 10,
      ...coordinates(palette.tone(index * 10)),
    })),
  }));
  const originalCustom = argbFromHex(input.customHex),
    custom = input.harmonize
      ? Blend.harmonize(originalCustom, source.toInt())
      : originalCustom;
  const customPalette = TonalPalette.fromInt(custom);
  const preview = {
    surface: hexFromArgb(scheme.surface),
    onSurface: hexFromArgb(scheme.onSurface),
    primary: hexFromArgb(scheme.primary),
    onPrimary: hexFromArgb(scheme.onPrimary),
  };
  const safePreview =
    opaqueColorRatio(preview.surface, preview.onSurface) >= 4.5 &&
    opaqueColorRatio(preview.primary, preview.onPrimary) >= 4.5;
  const capturedInput = {
    sourceMode: input.sourceMode,
    sourceHex: input.sourceMode === 'hex' ? input.sourceHex : null,
    requestedHct:
      input.sourceMode === 'hct'
        ? { hue: input.hue, chroma: input.chroma, tone: input.tone }
        : null,
    variant: input.variant,
    specVersion: input.specVersion,
    isDark: input.isDark,
    contrastLevel: input.contrastLevel,
    customHex: input.customHex,
    harmonize: input.harmonize,
  };
  return {
    library: '@material/material-color-utilities@0.4.0',
    input: capturedInput,
    requestedSpec: input.specVersion,
    effectiveSpec: scheme.specVersion,
    platform: scheme.platform,
    source: coordinates(source.toInt()),
    roles,
    palettes,
    custom: {
      original: coordinates(originalCustom),
      result: coordinates(custom),
      harmonized: input.harmonize,
      tones: Array.from({ length: 11 }, (_, index) => ({
        requestedTone: index * 10,
        ...coordinates(customPalette.tone(index * 10)),
      })),
    },
    preview,
    safePreview,
  };
}

Compiled explorer

tsx
'use client';
import { useEffect, useId, useRef, useState, type CSSProperties } from 'react';
import {
  hexFromArgb,
  sourceColorFromImage,
} from '@material/material-color-utilities';
import { Button } from '@mobtranslate/ui';
import { CodeBlock } from '../components/code-block';
import {
  buildColorProposal,
  initialColorInput,
  validateColorInput,
  type ColorProposalInput,
} from '../lib/color-proposal';
import './color-proposal.css';
import fixture from '../public/fixtures/color-source.json';

export function ColorProposalExample() {
  const id = useId();
  const [draft, setDraft] = useState(initialColorInput);
  const [coordinates, setCoordinates] = useState({
    hue: '49',
    chroma: '45',
    tone: '50',
  });
  const [proposal, setProposal] = useState(() =>
    buildColorProposal(initialColorInput),
  );
  const [roleName, setRoleName] = useState('primary'),
    [paletteName, setPaletteName] = useState('primary');
  const [ready, setReady] = useState(false),
    [status, setStatus] = useState(
      'Initial proposal. Shared Earth tokens are unchanged.',
    );
  const [extracting, setExtracting] = useState(false);
  const image = useRef<HTMLImageElement>(null),
    sourceEpoch = useRef(0),
    extractingRef = useRef(false);
  useEffect(() => {
    setReady(true);
    return () => {
      sourceEpoch.current++;
    };
  }, []);
  const number = (value: string) => (value.trim() === '' ? NaN : Number(value));
  const input: ColorProposalInput = {
    ...draft,
    hue: number(coordinates.hue),
    chroma: number(coordinates.chroma),
    tone: number(coordinates.tone),
  };
  const errors = validateColorInput(input),
    valid = errors.length === 0;
  const role =
    proposal.roles.find((candidate) => candidate.name === roleName) ??
    proposal.roles[0];
  const palette =
    proposal.palettes.find((candidate) => candidate.name === paletteName) ??
    proposal.palettes[0];
  const capture = () => {
    if (!valid) return;
    const next = buildColorProposal(input);
    setProposal(next);
    setRoleName((current) =>
      next.roles.some((candidate) => candidate.name === current)
        ? current
        : next.roles[0].name,
    );
    setStatus(
      'Captured a local color proposal. No shared token, source record or publication changed.',
    );
  };
  const extract = async () => {
    if (extractingRef.current || !image.current) return;
    const token = sourceEpoch.current,
      fixture = image.current;
    extractingRef.current = true;
    setExtracting(true);
    setStatus(
      'Reading the original local fixture and selecting a seed with the SDK.',
    );
    try {
      await fixture.decode();
      const sourceHex = hexFromArgb(await sourceColorFromImage(fixture));
      if (token !== sourceEpoch.current) {
        setStatus(
          'Source settings changed while extracting. Your draft source was retained.',
        );
        return;
      }
      setDraft((current) => ({ ...current, sourceMode: 'hex', sourceHex }));
      setStatus(
        `SDK selected ${sourceHex} from the local four-band fixture. Capture explicitly to generate a proposal.`,
      );
    } catch {
      if (token === sourceEpoch.current)
        setStatus(
          'Fixture seed extraction failed. The draft source and captured proposal are retained.',
        );
    } finally {
      extractingRef.current = false;
      setExtracting(false);
    }
  };
  const previewStyle = {
    backgroundColor: proposal.preview.surface,
    color: proposal.preview.onSurface,
    '--color-background': proposal.preview.surface,
    '--color-foreground': proposal.preview.onSurface,
    '--color-muted-foreground': proposal.preview.onSurface,
    '--color-primary': proposal.preview.primary,
    '--color-primary-foreground': proposal.preview.onPrimary,
    '--color-primary-hover': proposal.preview.primary,
    '--color-primary-active': proposal.preview.primary,
    '--color-ring': proposal.preview.onSurface,
  } as CSSProperties;
  const css = `/* Material role namespace only; not a complete MobTranslate theme. */\n.generated-color-proposal {\n${proposal.roles.map((value) => `  --proposal-${value.name.replaceAll('_', '-')}: ${value.hex};`).join('\n')}\n}`;
  const realized = (value: number) => value.toFixed(3);
  return (
    <div className="color-proposal-example space-y-4" data-color-proposal-ready={ready}>
      <p>
        Generate a captured proposal with Material Color Utilities 0.4.0. The
        algorithm’s platform parameter is phone; it does not detect your device
        or read a wallpaper. Preview theme is independent of this page’s
        light/dark theme.
      </p>
      <div>
        <img
          ref={image}
          src="/fixtures/color-source.svg"
          width={fixture.width}
          height={fixture.height}
          alt={fixture.description}
        />
        <p>
          Original vector fixture: {fixture.width}×{fixture.height} pixels. Band specification: {fixture.bands.map(band=>`${band.width}×${fixture.height} ${band.hex}`).join('; ')}. It is not a source photograph or cultural asset.
        </p>
        <div className="color-proposal-actions">
          <Button
            type="button"
            variant="outline"
            disabled={!ready}
            aria-disabled={extracting || undefined}
            aria-busy={extracting}
            data-color-extract
            onClick={extract}
          >
            Extract seed from local fixture
          </Button>
        </div>
      </div>
      <fieldset className="grid gap-4 sm:grid-cols-2">
        <legend className="font-semibold mb-3">Draft source and scheme</legend>
        <label htmlFor={`${id}-source-mode`}>
          Source mode
          <select
            id={`${id}-source-mode`}
            data-color-input="sourceMode"
            className="mt-input mt-2 w-full"
            disabled={!ready}
            value={draft.sourceMode}
            onChange={(event) => {
              sourceEpoch.current++;
              const sourceMode = event.currentTarget
                .value as ColorProposalInput['sourceMode'];
              setDraft((current) => ({ ...current, sourceMode }));
            }}
          >
            <option value="hex">Opaque sRGB hex</option>
            <option value="hct">Requested HCT coordinates</option>
          </select>
        </label>
        {draft.sourceMode === 'hex' ? (
          <label htmlFor={`${id}-source`}>
            Source hex
            <input
              id={`${id}-source`}
              data-color-input="sourceHex"
              className="mt-input mt-2 w-full"
              spellCheck={false}
              disabled={!ready}
              value={draft.sourceHex}
              aria-invalid={!/^#[\da-f]{6}$/i.test(draft.sourceHex)}
              aria-describedby={`${id}-help ${id}-errors`}
              onChange={(event) => {
                sourceEpoch.current++;
                const sourceHex = event.currentTarget.value;
                setDraft((current) => ({ ...current, sourceHex }));
              }}
            />
          </label>
        ) : (
          <div className="grid gap-3">
            {(['hue', 'chroma', 'tone'] as const).map((key) => (
              <label htmlFor={`${id}-${key}`} key={key}>
                Requested {key}
                <input
                  id={`${id}-${key}`}
                  data-color-input={key}
                  type="number"
                  min="0"
                  max={key === 'hue' ? 360 : key === 'chroma' ? 150 : 100}
                  step="any"
                  className="mt-input mt-2 w-full"
                  disabled={!ready}
                  value={coordinates[key]}
                  aria-invalid={
                    !Number.isFinite(input[key]) ||
                    input[key] < 0 ||
                    input[key] >
                      (key === 'hue' ? 360 : key === 'chroma' ? 150 : 100)
                  }
                  aria-describedby={`${id}-help ${id}-errors`}
                  onChange={(event) => {
                    sourceEpoch.current++;
                    const value = event.currentTarget.value;
                    setCoordinates((current) => ({ ...current, [key]: value }));
                  }}
                />
              </label>
            ))}
          </div>
        )}
        <label htmlFor={`${id}-variant`}>
          Scheme variant
          <select
            id={`${id}-variant`}
            data-color-input="variant"
            className="mt-input mt-2 w-full"
            disabled={!ready}
            value={draft.variant}
            onChange={(event) => {
              const variant = event.currentTarget
                .value as ColorProposalInput['variant'];
              setDraft((current) => ({ ...current, variant }));
            }}
          >
            <option value="tonal">Tonal spot</option>
            <option value="expressive">Expressive</option>
            <option value="fidelity">Fidelity</option>
          </select>
        </label>
        <label htmlFor={`${id}-spec`}>
          Requested specification
          <select
            id={`${id}-spec`}
            data-color-input="specVersion"
            className="mt-input mt-2 w-full"
            disabled={!ready}
            value={draft.specVersion}
            onChange={(event) => {
              const specVersion = event.currentTarget
                .value as ColorProposalInput['specVersion'];
              setDraft((current) => ({ ...current, specVersion }));
            }}
          >
            <option value="2025">2025</option>
            <option value="2021">2021</option>
          </select>
        </label>
        <label htmlFor={`${id}-contrast`}>
          Contrast level
          <select
            id={`${id}-contrast`}
            data-color-input="contrastLevel"
            className="mt-input mt-2 w-full"
            disabled={!ready}
            value={draft.contrastLevel}
            onChange={(event) => {
              const contrastLevel = Number(event.currentTarget.value);
              setDraft((current) => ({ ...current, contrastLevel }));
            }}
          >
            <option value="0">Standard (0)</option>
            <option value="0.5">Medium (0.5)</option>
            <option value="1">High (1)</option>
            <option value="-1">Low, experimental (−1)</option>
          </select>
        </label>
        <label className="flex items-center gap-3 min-h-11">
          <input
            data-color-input="isDark"
            type="checkbox"
            disabled={!ready}
            checked={draft.isDark}
            onChange={(event) => {
              const isDark = event.currentTarget.checked;
              setDraft((current) => ({ ...current, isDark }));
            }}
          />
          Generate a dark proposal
        </label>
        <label htmlFor={`${id}-custom`}>
          Custom color hex
          <input
            id={`${id}-custom`}
            data-color-input="customHex"
            className="mt-input mt-2 w-full"
            spellCheck={false}
            disabled={!ready}
            value={draft.customHex}
            aria-invalid={!/^#[\da-f]{6}$/i.test(draft.customHex)}
            aria-describedby={`${id}-help ${id}-errors`}
            onChange={(event) => {
              const customHex = event.currentTarget.value;
              setDraft((current) => ({ ...current, customHex }));
            }}
          />
        </label>
        <label className="flex items-center gap-3 min-h-11">
          <input
            data-color-input="harmonize"
            type="checkbox"
            disabled={!ready}
            checked={draft.harmonize}
            onChange={(event) => {
              const harmonize = event.currentTarget.checked;
              setDraft((current) => ({ ...current, harmonize }));
            }}
          />
          Harmonize this custom color toward the source
        </label>
      </fieldset>
      <p id={`${id}-help`}>
        Hex colors must use #RRGGBB. Requested hue is 0–360°, chroma 0–150 and
        tone 0–100. The sRGB gamut can reduce realized chroma; HCT is not HSL.
        Custom-color harmonization does not rewrite the generated semantic
        roles.
      </p>
      <p id={`${id}-errors`} data-color-errors>
        {valid
          ? 'Draft settings are valid; capture to apply them to the local proposal.'
          : errors.join(' ')}
      </p>
      <div className="color-proposal-actions">
        <Button
          type="button"
          data-color-capture
          disabled={!ready || !valid}
          onClick={capture}
        >
          Capture color proposal
        </Button>
      </div>
      <p role="status" data-color-status>
        {status}
      </p>
      <dl className="grid gap-3 sm:grid-cols-2" data-color-captured>
        <div>
          <dt>Captured variant/theme/contrast</dt>
          <dd>
            {proposal.input.variant}; {proposal.input.isDark ? 'dark' : 'light'}
            ; {proposal.input.contrastLevel}
          </dd>
        </div>
        <div>
          <dt>Requested → effective specification</dt>
          <dd data-color-spec>
            {proposal.requestedSpec} → {proposal.effectiveSpec}
          </dd>
        </div>
        <div>
          <dt>Realized source</dt>
          <dd data-color-source>{proposal.source.hex}</dd>
        </div>
        <div>
          <dt>Realized hue/chroma/tone</dt>
          <dd data-color-realized>
            {realized(proposal.source.hue)} / {realized(proposal.source.chroma)}{' '}
            / {realized(proposal.source.tone)}
          </dd>
        </div>
        <div>
          <dt>Captured custom color</dt>
          <dd data-color-custom>
            {proposal.custom.original.hex} → {proposal.custom.result.hex};
            harmonization {proposal.custom.harmonized ? 'enabled' : 'disabled'}
          </dd>
        </div>
        <div>
          <dt>Available role values</dt>
          <dd>
            {proposal.roles.length}, derived from the SDK’s current allColors
            collection
          </dd>
        </div>
      </dl>
      {proposal.input.requestedHct && (
        <p>
          Requested HCT: {proposal.input.requestedHct.hue} /{' '}
          {proposal.input.requestedHct.chroma} /{' '}
          {proposal.input.requestedHct.tone}. Compare the realized coordinates
          above; the request is not an achieved-gamut guarantee.
        </p>
      )}
      <p>
        SDK fallback is explicit. Some variants resolve a requested 2025 scheme
        through 2021 rules. A specification/variant name is not a conformance or
        contrast certificate.
      </p>
      {proposal.safePreview ? (
        <div
          data-color-preview
          className="color-proposal-preview"
          style={previewStyle}
        >
          <h3 className="font-semibold">Scoped action specimen</h3>
          <p>
            Only this surface and action use the captured role pairs.
            Hover/pressed keep the same fill here; a full state system needs
            additional design and verification.
          </p>
          <Button
            type="button"
            onClick={() =>
              setStatus(
                'Proposal action activated locally. The shared Earth theme and source records are unchanged.',
              )
            }
          >
            Activate proposal action
          </Button>
        </div>
      ) : (
        <p data-color-preview-suppressed>
          Text/action specimen suppressed: at least one proposed opaque text
          pair is below 4.5:1. All role values and measurements remain available
          in the neutral interface.
        </p>
      )}
      <label htmlFor={`${id}-role`}>
        Inspect a captured role
        <select
          id={`${id}-role`}
          data-color-role-select
          className="mt-input mt-2 w-full"
          value={role.name}
          onChange={(event) => setRoleName(event.currentTarget.value)}
        >
          {proposal.roles.map((value) => (
            <option key={value.name} value={value.name}>
              {value.name}
            </option>
          ))}
        </select>
      </label>
      <div data-color-role-detail>
        <p className="font-semibold">
          {role.name}: {role.hex}; realized tone {realized(role.tone)}
        </p>
        <span
          className="color-proposal-swatch"
          aria-hidden="true"
          style={{ backgroundColor: role.hex }}
        />
        <p>
          SDK background classification:{' '}
          {role.isBackground ? 'background' : 'not background'}. SDK
          contrast-curve target: {role.sdkTarget ?? 'not declared'}.
        </p>
        {role.backgrounds.length ? (
          <ul className="guidance-list">
            {role.backgrounds.map((background) => (
              <li key={background.name}>
                {background.name} ({background.hex}):{' '}
                {background.ratio.toFixed(3)}:1. AA normal-text threshold:{' '}
                {background.ratio >= 4.5 ? 'passes' : 'fails'}; 3:1 comparison:{' '}
                {background.ratio >= 3 ? 'passes' : 'fails'}.
              </li>
            ))}
          </ul>
        ) : (
          <p>
            No background relationship is declared for this role. The explorer
            does not invent a text pairing.
          </p>
        )}
      </div>
      <div
        className="doc-table"
        role="region"
        tabIndex={0}
        aria-label="Generated roles and declared background contrast"
      >
        <table>
          <thead>
            <tr>
              <th scope="col">Role</th>
              <th scope="col">Hex</th>
              <th scope="col">Declared backgrounds and ratios</th>
            </tr>
          </thead>
          <tbody>
            {proposal.roles.map((value) => (
              <tr key={value.name}>
                <th scope="row">{value.name}</th>
                <td>{value.hex}</td>
                <td>
                  {value.backgrounds.length
                    ? value.backgrounds
                        .map(
                          (background) =>
                            `${background.name}: ${background.ratio.toFixed(3)}:1`,
                        )
                        .join('; ')
                    : 'No declared pairing'}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <label htmlFor={`${id}-palette`}>
        Inspect a captured tonal palette
        <select
          id={`${id}-palette`}
          data-color-palette-select
          className="mt-input mt-2 w-full"
          value={palette.name}
          onChange={(event) => setPaletteName(event.currentTarget.value)}
        >
          {proposal.palettes.map((value) => (
            <option key={value.name} value={value.name}>
              {value.name}
            </option>
          ))}
        </select>
      </label>
      <p>
        Palette key color: {palette.keyColor.hex}; realized HCT{' '}
        {realized(palette.keyColor.hue)} / {realized(palette.keyColor.chroma)} /{' '}
        {realized(palette.keyColor.tone)}.
      </p>
      <div
        className="doc-table"
        role="region"
        tabIndex={0}
        aria-label="Captured tonal palette"
      >
        <table>
          <thead>
            <tr>
              <th scope="col">Requested tone</th>
              <th scope="col">Realized hex/tone/chroma</th>
              <th scope="col">Swatch</th>
            </tr>
          </thead>
          <tbody>
            {palette.tones.map((value) => (
              <tr key={value.requestedTone}>
                <th scope="row">{value.requestedTone}</th>
                <td>
                  {value.hex}; {realized(value.tone)}; {realized(value.chroma)}
                </td>
                <td>
                  <span
                    className="color-proposal-swatch"
                    aria-hidden="true"
                    style={{ backgroundColor: value.hex }}
                  />
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <details data-color-full>
        <summary className="min-h-11 cursor-pointer">
          Full captured request, roles, palettes and custom color
        </summary>
        {ready ? <CodeBlock language="json" code={JSON.stringify(proposal, null, 2)} /> :
          <p role="status">Preparing the full-precision captured output.</p>}
      </details>
      <details>
        <summary className="min-h-11 cursor-pointer">
          Role namespace CSS export
        </summary>
        <CodeBlock language="css" code={css} />
      </details>
      <p className="text-sm">
        All measurements use opaque sRGB role values. They do not cover
        overlays, every interaction state, color perception or cultural
        authority. Extraction reads only the original local fixture; wallpaper,
        browser personal color and remote source records are not read.
      </p>
    </div>
  );
}

Scoped control CSS

css
.color-proposal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.color-proposal-example fieldset,
.color-proposal-example label,
.color-proposal-example input,
.color-proposal-example select {
  min-inline-size: 0;
  max-inline-size: 100%;
}
.color-proposal-example p,
.color-proposal-example dt,
.color-proposal-example dd,
.color-proposal-example summary {
  overflow-wrap: anywhere;
}
.color-proposal-example dl > div {
  min-inline-size: 0;
}
.color-proposal-actions .mt-btn,
.color-proposal-preview .mt-btn {
  min-block-size: 44px;
  block-size: auto;
  max-inline-size: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
}
.color-proposal-preview {
  padding: 16px;
  border: 1px solid currentColor;
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 12px;
  min-inline-size: 0;
}
.color-proposal-preview p,
.color-proposal-preview h3 {
  overflow-wrap: anywhere;
}
.color-proposal-swatch {
  display: inline-block;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--color-muted-foreground);
  vertical-align: middle;
}

Original extraction fixture

The 64×64 authored vector is the only image source read by the extraction action. The swatch array and dimensions are explicit, and the chosen seed is reported as an algorithm result rather than a source identity or endorsement.

svg
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect width="16" height="64" fill="#b45e2a"/><rect x="16" width="16" height="64" fill="#386b4f"/><rect x="32" width="16" height="64" fill="#395d75"/><rect x="48" width="16" height="64" fill="#faf8f5"/></svg>

Verification and failure cases

  • Compare generated role values with the pinned SDK, including light/dark, variants, contrast levels and specification fallback.
  • Compare requested versus realized high-chroma/light-tone HCT; verify impossible gamut requests are not labelled exact.
  • Capture, edit the draft and inspect the retained JSON. Blank/invalid active settings must leave the capture unchanged.
  • Extract from the actual fixture; compare the selected seed with the quantizer/ranking over its known pixels. Exercise image/canvas failure and held extraction with a competing source edit.
  • Toggle harmonization and verify custom output changes independently of generated roles.
  • Activate the local specimen and inspect computed colors; confirm root Earth tokens and unrelated controls remain unchanged.
  • Check both global themes, independently selected proposal theme, 320px, text enlargement, keyboard fields/actions/full exports and accessibility scenes.
  • Do not claim generated values or nominal SDK targets prove every production state, transparency layer, perceptual need or cultural authority.