01 · Seed color
Presets · tap to load
HCT picker · pick by Hue, Chroma, Tone

M3 uses HCT (perceptual color space) internally. Hue is the colour family (0–360°), Chroma is the saturation, Tone is the perceived lightness (0–100).

Pick a seed from an image

Same pipeline Android 12+ uses for Material You: extracts the most representative colour from the image and uses it as the seed.

Drop an image or click to upload
Tonal palettes · override individual tones

Each role is computed from a specific tone of one of these five palettes. Click any swatch to override it. The error palette is fixed by M3 and isn't customisable.

02 · Preview (light and dark)
03 · Output

                

A theme from one color

Pick a hex, get back thirty colors. That's the promise of Material 3, and the part that makes it polarising among Android developers. Some love the speed; others miss the control of hand-picking every color in a brand guideline. Whichever camp you're in, the algorithm doing the work is genuinely interesting, and worth knowing if you ever need to debug why your "perfect" seed color produced a theme that looks slightly off.

The trick is a color space Google built specifically for this called HCT (Hue, Chroma, Tone). The point of HCT is that Tone is calibrated to how light a color looks, not how light it measures. Two colors at tone 50 will appear roughly equally bright to the eye even if one is bright cyan and the other is dark red. RGB doesn't do that. HSL tries but isn't great at it. HCT, more or less, does. That's why generated M3 themes feel balanced where hand-rolled "I'll just darken the brand color by 30%" themes often don't.

When you give the algorithm a seed, it builds five tonal palettes (Primary, Secondary, Tertiary, Neutral, Neutral Variant), each containing 13 specific tones from 0 (pure black) to 100 (pure white) at irregular intervals: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100. The Primary palette keeps your seed's hue. Secondary and Tertiary shift the hue by amounts that produce visual variety without clashing. The two Neutral palettes are low-chroma, close to gray, but with just a hint of your seed's hue so the surface colors feel like they belong to the same theme rather than being random gray.

Your primary color isn't a tonal palette itself. It's a specific point on one. In the light theme it's the Primary palette at tone 40 (medium-dark), and onPrimary (text that sits on top of it) is tone 100 (white). primaryContainer is tone 90 (very light), and onPrimaryContainer is tone 10 (very dark). The dark theme inverts most of this. primary becomes tone 80, onPrimary becomes tone 20. Once you see the pattern, the 30-odd color role names stop feeling like a maze.

What the role names mean in practice

The names follow a system, even if the system is at first opaque. Anything called primary is your dominant brand color: filled buttons, the FAB, active state indicators. onPrimary is whatever sits on top of a primary background. The algorithm guarantees it has legal contrast. primaryContainer is a much softer version meant for less prominent containers like chip backgrounds or the background of a navigation rail. onPrimaryContainer goes on top of that. Once you've internalised the pattern, Secondary and Tertiary follow the same shape. Secondary is for medium-emphasis accents. Tertiary is a contrasting accent for things that should pop against your primary scheme.

The Error roles work the same way (error, onError, errorContainer, onErrorContainer), though they don't follow your seed. They're derived from a fixed red. That's intentional. Users associate red with errors regardless of your brand color, and overriding that costs more than it buys.

The surface roles are where most of your UI actually lives, because most of your UI is cards, sheets, menus, and backgrounds. surface with onSurface is the workhorse pair, by far the most-used in any real app. surfaceVariant is a slightly tinted surface for visual hierarchy (outlined text fields, dividers, secondary surfaces). background is often the same color as surface in M3, which feels redundant the first time you see it, but Google kept both for Material 2 migration compatibility. outline and outlineVariant are for borders and dividers. The variant is softer for decorative borders that shouldn't dominate. inverseSurface / inverseOnSurface / inversePrimary are for components that deliberately contrast with their context. The classic example is a Snackbar: dark on a light app, light on a dark app. scrim is the dimming overlay behind modal dialogs. And surfaceTint is the M3 elevation overlay. Google removed shadows from elevation in M3 and replaced them with a faint primary tint, a decision that mostly works in light mode and is at best controversial in dark.

Dropping the output into your project

The Kotlin code from the tool above replaces the contents of app/src/main/java/com/example/yourapp/ui/theme/Theme.kt, or wherever your project keeps it. The two colorScheme values (LightColors, DarkColors by convention) get pasted in. The outer MyAppTheme Composable that wraps MaterialTheme stays as it was. Then your activity calls it:

setContent {
    MyAppTheme {
        // Your composables here
    }
}

In your composables, always read colors from MaterialTheme.colorScheme.primary. Never hard-code Color(0xFF6750A4). This is non-negotiable, because the whole point of having a theme is that dark mode just works. Hard-coding any color anywhere in your UI breaks that, and you'll only notice when a user complains about eye-searing white in dark mode after they've already left a one-star review.

If you're coming from Material 2

The migration from M2's colors to M3's colorScheme isn't a one-to-one rename, which catches a lot of people. primary stays primary, but primaryVariant doesn't become anything obvious. In M3 the closest analog is primaryContainer, but the meaning shifted. In M2, the variant was "darker primary, for elevated app bars." In M3, the container version means "softer primary, for less-prominent UI." It's not a darker shade. It's a different role. The same shift happened for Secondary. Tertiary is new. M2 had no third accent at all. M3 introduces it specifically for callouts that need to stand apart from your primary scheme.

A more subtle change: M2 handled "medium emphasis text" by taking onSurface and applying an alpha of 0.74 (or whatever the recommendation of the year was). M3 introduces onSurfaceVariant as a dedicated lower-emphasis text color, no alpha required. This matters because alpha-blended text doesn't render the same on every device, and contrast checkers can't verify blended text the way they can verify a solid color. Use onSurfaceVariant and don't reach for the alpha modifier anymore.

Elevation is the other thing that shifted. M2 used drop shadows. M3 uses the surfaceTint overlay. Your surface gets tinted progressively more by your primary color as it elevates. Shadows still work if you set them explicitly, but they're no longer the default. This is the M3 design choice I see developers complain about most, especially for apps that look right in light mode but feel flat in dark where the tint is barely visible. If that's you, override tonalElevation on cards and sheets back to zero and use shadowElevation manually. Material gives you both knobs. You don't have to pick the default.

Things that will bite you

Picking a pale seed color is the most common mistake. The algorithm assumes you've given it a saturated starting point. If your brand color is something like #FFE0E0 (washed-out pink), the entire theme comes out washed-out, and no amount of contrast checking will save you. Pick something with real chroma. The algorithm handles tone scaling for you. If your brand is genuinely pastel, pick the more-saturated version of the same hue as the seed, then accept that the generated theme will be more vivid than the brand color alone.

Don't try to bypass the algorithm by editing the generated colors by hand. The whole contract is that onPrimary has legal contrast against primary because it was computed from the palette. The moment you change one in isolation, you may have created an accessibility issue invisible until a user with low vision reports it. If you need to change the result, change the seed and regenerate the whole thing.

The contrast issue can also bite the other direction. A theme that looks fine in light mode can have surprising contrast problems in dark mode. The preview above shows both for exactly this reason. Run your seed through it twice. Once for the primary brand color, once for any secondary brand colors you want to expose. And actually look at the dark version, not just the light.

Brand-specific accent colors (custom warning yellow, a not-quite-primary brand green) need harmonize() applied to them, otherwise they'll clash with the generated palette. The Compose extension shifts the input color slightly toward your seed's hue without changing its identity meaningfully. Skip this and your custom warning yellow can look weirdly out of place next to the algorithm-generated yellows in your theme.

What this tool doesn't generate

The output is the standard M3 1.x color scheme. What lightColorScheme() and darkColorScheme() currently accept. Material 3 1.2 added the "Expressive" surface container hierarchy (surfaceContainerLowest through surfaceContainerHighest) for finer-grained surface elevation. The tool doesn't currently emit those. They're computed the same way (additional Neutral palette tones), but I haven't added them because most apps in the wild are still on M3 1.0 / 1.1 and the new roles are optional even on 1.2+. If you want them, the algorithm to derive them from your seed is in material-color-utilities. I'll add them here if there's demand.

The bigger thing missing is Dynamic Color, what Google calls Material You, where the user's wallpaper provides the seed. Dynamic Color can't be statically generated because the wallpaper isn't known at build time. The right pattern is to use this tool's output as a fallback for devices that don't support Material You (Android < 12), and call dynamicLightColorScheme(context) / dynamicDarkColorScheme(context) on devices that do. Most production apps end up doing both, with an if-check on Android version.

By Belchior · Last updated · May 2026

Copied