AngularMaterial DesignThemingCSS VariablesDesign Tokens

Angular Material 3: Theming with Tokens

U
UI Engineer
Featured Guide 15 min read

M3 is not just a redesign.

Angular Material 2 relied on static SCSS compilation. If you wanted "Dark Mode", you had to generate a second CSS file or duplicate classes.

Material 3 is built on CSS Custom Properties (Tokens). You change one variable in JS, and the entire app updates instantly.

02. --md-sys-color-primary

The new system exposes semantic tokens. You don't use "Blue-500". You use "Primary", "On-Primary", "Surface-Container".

// globals.css
:root
{'{'}
  --md-sys-color-primary: #6750A4;
  --md-sys-color-on-primary: #FFFFFF;
{'}'}

// Dark Mode Override
body.dark
{'{'}
  --md-sys-color-primary: #D0BCFF;
{'}'}

Deep Dive: HCT Color Space

Material 3 isn't just random hex codes. It uses the HCT (Hue Chroma Tone) color space.
This mathematically guarantees contrast. "Tone 40" text on "Tone 90" background always meets WCAG AA standards, regardless of the Hue (Blue, Red, or Green).

04. The Senior Engineer's Take

White Labeling Dream

If you build B2B apps, M3 is a lifesaver. You can fetch a customer's specific brand color from the backend and set it via document.body.style.setProperty() on load.

The entire Material library (Buttons, Inputs, Checkboxes) will respect that runtime value.

Interactive Playground

import React, { useState } from 'react';

// 🎨 Theme Visualizer

export default function ThemeDemo() {
    const [isDark, setIsDark] = useState(false);
    const [primary, setPrimary] = useState('#6366f1'); // Default Indigo
    
    // Simulate runtime token application
    const style = {
        '--app-primary': primary,
        '--app-bg': isDark ? '#0f172a' : '#ffffff',
        '--app-text': isDark ? '#ffffff' : '#0f172a',
        '--app-surface': isDark ? '#1e293b' : '#f1f5f9',
    };

    return (
        <div className="bg-slate-50 dark:bg-slate-950 p-8 rounded-3xl border border-slate-200 dark:border-slate-800 shadow-xl" style={style}>
             <div className="flex justify-between items-center mb-10">
                <h3 className="text-2xl font-black text-gray-900 dark:text-white flex items-center gap-3">
                    <span className="text-pink-500">🎨</span> Material 3 Tokens
                </h3>
            </div>
            
            <div className="flex flex-col md:flex-row gap-8">
                
                {/* Controls */}
                <div className="w-full md:w-1/3 bg-white dark:bg-slate-900 p-6 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm">
                    <div className="mb-6">
                        <label className="text-xs font-bold text-gray-500 uppercase block mb-3">Mode</label>
                        <div className="flex gap-2">
                            <button onClick={() => setIsDark(false)} className={`flex-1 py-2 rounded-lg flex items-center justify-center gap-2 ${!isDark ? 'bg-gray-200 dark:bg-slate-700 font-bold' : 'border border-gray-200 dark:border-slate-700'}`}>
                                <span>☀️</span> Light
                            </button>
                            <button onClick={() => setIsDark(true)} className={`flex-1 py-2 rounded-lg flex items-center justify-center gap-2 ${isDark ? 'bg-gray-200 dark:bg-slate-700 font-bold' : 'border border-gray-200 dark:border-slate-700'}`}>
                                <span>🌙</span> Dark
                            </button>
                        </div>
                    </div>

                    <div>
                         <label className="text-xs font-bold text-gray-500 uppercase block mb-3">Brand Color (--primary)</label>
                         <div className="grid grid-cols-4 gap-2">
                             {['#6366f1', '#ec4899', '#22c55e', '#eab308'].map(c => (
                                 <button 
                                    key={c}
                                    onClick={() => setPrimary(c)}
                                    className="aspect-square rounded-full border-2 border-white dark:border-slate-800 shadow relative transition-transform active:scale-90"
                                    style={{ backgroundColor: c }}
                                 >
                                     {primary === c && <span className="text-white absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"></span>}
                                 </button>
                             ))}
                         </div>
                    </div>
                </div>

                {/* Preview App */}
                <div className="flex-1 rounded-2xl shadow-2xl p-6 border transition-colors duration-300" 
                     style={{ backgroundColor: 'var(--app-bg)', color: 'var(--app-text)', borderColor: 'var(--app-surface)' }}>
                    
                    <div className="flex items-center justify-between mb-8">
                        <div className="font-bold text-xl">My App</div>
                        <div className="w-8 h-8 rounded-full" style={{ backgroundColor: 'var(--app-primary)' }}></div>
                    </div>

                    <div className="space-y-4">
                        <div className="p-4 rounded-xl transition-colors duration-300" style={{ backgroundColor: 'var(--app-surface)' }}>
                            <div className="font-bold mb-2">Primary Button</div>
                            <button className="px-4 py-2 rounded-lg text-white font-bold shadow-lg transition-all active:scale-95" style={{ backgroundColor: 'var(--app-primary)' }}>
                                Save Changes
                            </button>
                        </div>

                        <div className="p-4 rounded-xl transition-colors duration-300" style={{ backgroundColor: 'var(--app-surface)' }}>
                             <div className="font-bold mb-2">Tonal Button</div>
                             <div className="flex items-center gap-4">
                                <button className="px-4 py-2 rounded-lg font-bold" style={{ backgroundColor: 'var(--app-primary)', opacity: 0.2, color: 'var(--app-primary)' }}>
                                    Cancel
                                </button>
                                <span className="text-xs opacity-50">Background follows Primary with opacity</span>
                             </div>
                        </div>
                        
                        <div className="p-4 rounded-xl transition-colors duration-300" style={{ backgroundColor: 'var(--app-surface)' }}>
                             <div className="font-bold mb-2">Checkbox</div>
                             <div className="flex items-center gap-2">
                                 <div className="w-6 h-6 rounded flex items-center justify-center text-white" style={{ backgroundColor: 'var(--app-primary)' }}>
                                     <span></span>
                                 </div>
                                 <span className="opacity-80">Agree to Terms</span>
                             </div>
                        </div>
                    </div>

                </div>

            </div>
        </div>
    );
}