All posts
10 October 2026

Four apps on Vuetify 4: what the official codemod leaves behind

I moved four older Nuxt applications from Vuetify 3 to Vuetify 4. The official vuetify-codemods tool does most of the work, but not all of it. Here is the procedure that worked and the two scripts that saved me the most time.

First check that the application builds at all

Older repositories have rotted dependencies that have nothing to do with Vuetify. One of my applications did not build even before the migration, because its translation module had gone stale. Had I started with Vuetify, I would have looked for the fault in the wrong place.

So: a new branch, npm install, npx nuxt build. Only when that passes is it worth going on.

The steps

  1. npm install vuetify@^4 (and the new vuetify-nuxt-module, if you use it).
  2. Change the style entry vuetify/lib/styles/main.sass to vuetify/styles.
  3. Run the codemods on every folder with .vue files.
  4. Fix what the codemods left out.
  5. Build, run, and walk through the application in a browser.

The codemod wants a terminal

vuetify-codemods is interactive: it asks which changes to apply and refuses to start without a terminal. So it will not simply run in a script or in CI. Give it a pseudo-terminal and send Enter every second, since all the changes are preselected:

#!/bin/sh
GLOB="${1:-{pages,components,layouts}/**/*.vue}"
(for i in $(seq 1 60); do sleep 1; printf '\r'; done) \
  | script -qec "npx --yes vuetify-codemods --files '$GLOB'" /dev/null

Mind the scope. The default pattern covers pages, components and layouts. But .vue files live elsewhere too: in modules, shared, features, or in the root. Run the codemod on each such folder.

Classes the codemod leaves alone

The codemod renames typography classes in templates. It skips them in three cases:

  • in <style> blocks,
  • in class names built in scripts,
  • in files it cannot parse.

A short script that walks the files tracked by git is enough:

import pathlib
import re
import subprocess

CLASSES = {
    "text-h1": "text-display-large", "text-h2": "text-display-large", "text-h3": "text-display-medium",
    "text-h4": "text-headline-large", "text-h5": "text-headline-small", "text-h6": "text-title-large",
    "text-subtitle-1": "text-body-large", "text-subtitle-2": "text-title-small",
    "text-body-1": "text-body-large", "text-body-2": "text-body-medium",
    "text-caption": "text-body-small", "text-overline": "text-label-medium",
}
PATTERN = re.compile(r"(?<![\w-])(" + "|".join(sorted(CLASSES, key=len, reverse=True)) + r")(?![\w-])")

files = subprocess.run(["git", "ls-files", "*.vue", "*.css", "*.scss", "*.ts"], capture_output=True, text=True).stdout.split("\n")
for name in filter(None, files):
    path = pathlib.Path(name)
    if not path.exists():
        continue
    before = path.read_text(encoding="utf-8")
    after = PATTERN.sub(lambda match: CLASSES[match.group(1)], before)
    if after != before:
        path.write_text(after, encoding="utf-8")
        print(name)

The mapping is not one to one. Vuetify 4 moved to the Material Design 3 scale, so both text-h1 and text-h2 end up as text-display-large. Look at your headings afterwards. The sizes shift a little.

Traps

  • git add -A in an old repository. Older projects often lack .output and .nuxt in .gitignore. I once committed a whole build that way. Add files by name.
  • A tracked .env. Before you write local settings, check git ls-files .env. One of the projects had it in git.
  • Deploying without the local environment. After the migration one application ran only with a variable I had set locally. Without it every page returned 500, and the automatic deployment would have taken production down. Run the application once without your local variables before merging.

How long it takes

Vuetify itself is about an hour for a small application. The stale dependencies around it take longer, and so does the check in a browser, which no codemod replaces.

© 2026 Myrha Software