Skip to content

Material Design 3 (MD3) in Vuetify is not a superficial skin. It formalizes tokens, revisits elevation & color semantics, reduces default density, and introduces more flexible theming primitives. This post compresses what matters when you are maintaining an MD2 codebase and need to sequence an incremental (not big‑bang) migration.

1. Quick Mental Model

AspectMD2 (Vuetify 2)MD3 (Vuetify 3)Why It Matters
Color SystemStatic palette + variantsSource color → dynamic tonal palettesEnables auto light/dark & accessibility tuning
TypographyPredefined classes (display-4, etc.)Design token driven scales (text-h1, semantic roles)Consistency + easier global adjustments
DensityGenerally denser defaultsMore spacious, touch-friendly spacingLayout shifts; affects above-the-fold content
Elevation & SurfacesShadow centricSurface color + shadow interplay, elevation tokensVisual hierarchy depends more on color layers
Theming API$vuetify.theme.themes.light.primary style overridesToken-centric: createTheme, CSS vars (--v-theme-*)Simpler dynamic runtime adjustments
ComponentsSome monolithic componentsMore composable primitives (e.g. layout, navigation)Encourages smaller building blocks
IconsImplicit default setsExplicit icon aliasing & sets configNeed to register custom / migrated icons

2. Color & Theming Deep Dive

MD3 uses a source (seed) color to algorithmically derive tonal ranges (0–100). Vuetify exposes these as CSS variables like --v-theme-primary plus tonal variants (e.g. primary-darken-1 becomes more systematic).

Example MD3 theme creation (Vuetify 3):

ts
// theme.ts
import { createVuetify } from 'vuetify';
import 'vuetify/styles';

const customLight = {
  dark: false,
  colors: {
    primary: '#4F46E5', // seed
    secondary: '#0EA5E9',
    error: '#DC2626',
    success: '#059669'
  },
  variables: { // optional extra design tokens
    'border-radius-sm': '4px'
  }
};

export const vuetify = createVuetify({
  theme: {
    defaultTheme: 'customLight',
    themes: { customLight },
  }
});

Legacy (MD2) override example for comparison:

ts
// Vuetify 2 (MD2 style)
const vuetify = new Vuetify({
  theme: {
    themes: {
      light: {
        primary: '#4F46E5',
        secondary: '#0EA5E9'
      }
    }
  }
});

Migration tip: Start by introducing a parallel theme module while leaving existing SCSS overrides intact. Then progressively replace hard-coded palette references (e.g. primary lighten-2) with semantic usage via CSS vars.

3. Typography

MD2 relied on utility classes like display-1, headline, subtitle-2. MD3 encourages a semantic scale (e.g. h1, title-large, body-medium) which Vuetify maps to CSS variables.

Strategy:

  1. Inventory current typography classes (grep for display-, headline, subtitle, overline).
  2. Map each to new semantic target (e.g. display-1text-h3 depending on visual weight).
  3. Create a temporary mapping layer (custom classes) to avoid massive template churn in one PR.

Example mapping SCSS:

scss
/* transitional-typography.scss */
.display-1 { @extend .text-h3; }
.subtitle-2 { @extend .text-subtitle-2; }

Then gradually replace usages with native MD3 classes.

4. Density & Spacing

MD3 defaults may make app sections feel “looser.” Resist the urge to globally compress; validate real user tasks first. Where tighter density is truly needed (e.g. data grids), apply local density modifiers instead of global resets to preserve accessibility.

Checklist:

  • Critical viewport benchmarks (above fold) after switch.
  • Forms with many fields—review vertical rhythm.
  • Icon buttons: confirm size and density props (Vuetify 3 exposes more explicit sizing props).

5. Elevation, Surfaces & Tone

MD3 uses color layering + subtle shadow. Hard-coded elevation-24 rarely makes sense now.

Refactor approach:

  1. Audit components using high elevation values.
  2. Replace with lower tokens or rely on surface differentiation (bg-surface-variant).
  3. Avoid mixing custom large box-shadows with MD3 surfaces—they'll look inconsistent.

6. Component API Shifts (Representative)

ComponentMD2 PatternMD3 / Vuetify 3 PatternMigration Note
App Layoutv-app-bar, v-navigation-drawer tightly coupledMore composable layout wrappers + slotsRe-think nested nav patterns
Buttonsv-btn color="primary" depressedv-btn variant="flat|tonal|elevated|outlined|text"Map old style props → new variant
IconsImplicit via v-iconRegister sets & aliases in Vuetify configConsolidate icon strategy early
FormsLegacy validation propsComposition API utilities & v-form events refinedRefactor progressively; wrap old forms
Data TablesMonolithicVirtualization & lab features separatedEvaluate feature parity before swapping

7. Progressive Migration Strategy

Avoid a freeze & rewrite. Use strangler technique.

PhaseGoalKey ActionsExit Criteria
0 BaselineMeasurementCapture CLS/LCP, bundle size, critical flowsMetrics dashboard exists
1 Dual ThemeIntroduce Vuetify 3 in parallel (if separate branch/app shell)Create MD3 theme, mount a sandbox routeSandbox route stable
2 Token LayerAbstract colors/typography behind tokensAdd CSS var fallbacks for MD280% hard-coded colors removed
3 Component IslandsMigrate leaf components first (buttons, badges)Provide adapter props layerNo visual regressions in migrated islands
4 Layout & NavRework navigation & app barsImplement MD3 density/elevation defaultsNavigation UX parity
5 Complex WidgetsTables, forms, dialogsReplace or wrap with adaptersFeature parity + a11y test pass
6 CleanupRemove transitional shimsDelete mapping SCSS & alias utilitiesNo deprecated classes remain

8. Code Example: Adapter Wrapper

Create wrappers so templates stay stable while you refactor internals.

vue
<!-- components/AdaptiveButton.vue -->
<script setup lang="ts">
import { computed } from 'vue';
interface Props { legacyStyle?: 'depressed' | 'text' | 'outlined'; color?: string }
const props = defineProps<Props>();
const variant = computed(() => {
  switch (props.legacyStyle) {
    case 'depressed': return 'flat';
    case 'outlined': return 'outlined';
    case 'text': return 'text';
    default: return 'elevated';
  }
});
</script>
<template>
  <v-btn :color="props.color" :variant="variant"> <slot /> </v-btn>
</template>

Usage during migration:

vue
<AdaptiveButton legacyStyle="depressed" color="primary">Save</AdaptiveButton>

Later you remove legacyStyle once all calls adopt variant directly.

9. Accessibility (A11y) Considerations

  • Re-test color contrast after dynamic tonal palette adoption (light & dark modes).
  • Validate focus rings: MD3 emphasizes visible focus—ensure custom overrides did not suppress them.
  • Density reductions may help touch users but verify keyboard navigation order after layout changes.

10. Performance Notes

Vuetify 3 (Vite + tree-shaking) can reduce bundle size if you eliminate legacy plugin bloat. Track:

  • Initial bundle diff (before/after enabling Vuetify 3 tree-shakable components)
  • Hydration time (if SSR)
  • Theme switch latency (should be near-instant with CSS vars)

11. Risk Radar & Mitigations

RiskImpactMitigation
Design DriftVisual inconsistency across mixed MD2/MD3 zonesIntroduce a migration banner in internal environments; track pages migrated
RegressionsSubtle layout/breakpointsVisual regression tests (Percy/Chromatic) on critical flows
Churn FatigueDeveloper slowdownKeep phases <2 weeks; publish migration scoreboard
A11y GapsMissed contrast / focusAdd automated a11y scan (axe) to CI per PR
Scope CreepOver-refactoringTime-box: only refactor unrelated code if blocking migration

12. Migration Checklist (Copy/Paste)

text
[ ] Baseline performance + visual snapshots captured
[ ] MD3 theme scaffolded with source color + dark variant
[ ] Transitional typography mapping layer added
[ ] Color hard-codes replaced by semantic tokens (>=80%)
[ ] Adapter components for buttons & icons
[ ] First leaf component group migrated & validated
[ ] Navigation/layout shells refactored
[ ] Complex widgets (tables/forms) parity achieved
[ ] A11y audit (contrast, focus, landmarks)
[ ] Remove transitional shims / dead classes
[ ] Documentation updated for new tokens & variants

13. When NOT to Migrate (Yet)

Delay if:

  • You depend on MD2-only community components not yet ported.
  • Release cadence is mid critical feature cycle (avoid coupling risk).
  • Design team has not re-issued updated spacing/typography tokens.

14. Summary

Treat MD3 migration as design token adoption + component API modernization rather than a visual repaint. Sequence your work so user value continues shipping while the foundation evolves. Start small, measure, iterate.

Happy building—ship incremental value, not migration debt.

Random thoughts on Tech