How Vite resolves relative and absolute paths

Hello everyone! If you're working with Vite and handling static assets like fonts or images, path resolution can be tricky — especially when things work in one environment but not in others. In this article, I’ll explain you how Vite builds projects, the difference between absolute and relative paths, and share a real-life bug I encountered that illustrates why this stuff matters. How Vite Builds Projects When you run vite build, Vite compiles, bundles, and optimizes your app into a production-ready folder called dist/. This folder is what you deploy — everything else stays behind. Here’s an example of a project structure before build: project-folder/ ├── public/ │ └── fonts/ │ └── icon-font.woff │ ├── src/ │ ├── main.js │ ├── styles/ │ │ └── app.less │ └── components/ │ └── ... │ ├── index.html ├── vite.config.js └── package.json After building, your dist/ folder might look like this: dist/ ├── assets/ │ ├── app-9f3a2b4d.js │ ├── style-2c1f6a7e.css │ └── vendor-1a2b3c4d.js └── fonts/ └── icon-font.woff What’s in the dist/ folder: assets/: contains final, optimized JS and CSS files: app-xxxx.js – main application bundle style-xxxx.css – compiled styles from .less, .scss, or .css files vendor-xxxx.js – third-party libraries fonts/: Includes static assets (like fonts) copied from the public/ folder Key Notes: The src/ folder and any source files (like app.less) are not included in the build. All filenames are hashed for cache busting, ensuring browsers load the latest version after deployments. This build is fully static and ready to be hosted on any static server (Netlify, Vercel, GitHub Pages, etc.) How Absolute Paths Work in Vite In Vite, an absolute path starts with a /. For example: @icon-font-path: "/fonts/icon-font.woff"; The leading slash tells the browser (and Vite) to look in the root of the site. Here’s how Vite resolves it: In development (vite dev): Vite serves files in the public/ directory at the root level. So /fonts/icon-font.woff is resolved as public/fonts/icon-font.woff. In production (vite build): Vite maintains this path, assuming that fonts/icon-font.woff will be at the root of the deployed dist/ folder. This means that any asset placed in the public/ folder can be accessed directly using an absolute path that starts with /. How Relative Paths Work in Vite A relative path points to a file based on the current file’s location. For example: @icon-font-path: "../fonts/icon-font.woff"; Relative paths work well when you're referencing files inside the src/ folder, as long as the file structure matches and Vite can bundle those files correctly. However, issues can arise if the relative path doesn't correctly reflect the file system, especially if you're referencing something that lives outside of src/, such as in public/. Real-life example: Why the relative path failed Let’s look at a bug I encountered on a new project. Here's the structure of the project: project-folder/ │ ├── public/ │ └── fonts/ │ └── icon-font.woff │ ├── src/ │ └── styles/ │ └── app.less │ Originally, in app.less a relative path was used: @icon-font-path: "../fonts/icon-font.woff"; This path worked fine in production and some environments, but when I ran the project locally with vite dev, the icon fonts didn’t load. Why it failed locally: The font folder was in the public/ directory. Relative paths from within src/ can’t access files in public/. So the file was simply not found. Why it worked in production: Vite was bundling everything, and the compiled CSS or JS file (e.g. app-9f3a2b4d.js) ended up inside the dist/assets/ folder. Meanwhile, the font file from the public/fonts/ directory was copied into dist/fonts/. The resulting dist/ structure looked like this: dist/ ├── assets/ │ ├── app-9f3a2b4d.js │ ├── style-2c1f6a7e.css │ └── vendor-1a2b3c4d.js └── fonts/ └── icon-font.woff The path used in the compiled CSS or JS was relative — and since those files were in dist/assets/, a path like ../fonts/icon-font.woff would still point correctly to dist/fonts/. In other words, from assets/style-2c1f6a7e.css, the browser goes one level up (../) and finds the fonts/ folder right there at the root of dist/. The path technically worked in production, but it was coincidental. The structure happened to align, not because the relative path was correct in source files, but because the build output ended up that way. This is why the path didn’t work in vite dev, where files aren't organized the same way — which leads to the next point: using absolute paths ensures consistency across all environments. The Fix: Switching to an absolute path did the trick: @icon-font-path: "/fonts/icon-font.woff"; Now Vite correctly served the font from public/, and the path worked i

Apr 11, 2025 - 21:32
 0
How Vite resolves relative and absolute paths

Hello everyone!

If you're working with Vite and handling static assets like fonts or images, path resolution can be tricky — especially when things work in one environment but not in others. In this article, I’ll explain you how Vite builds projects, the difference between absolute and relative paths, and share a real-life bug I encountered that illustrates why this stuff matters.

How Vite Builds Projects

When you run vite build, Vite compiles, bundles, and optimizes your app into a production-ready folder called dist/. This folder is what you deploy — everything else stays behind.

Here’s an example of a project structure before build:

project-folder/
├── public/
│   └── fonts/
│       └── icon-font.woff
│
├── src/
│   ├── main.js
│   ├── styles/
│   │   └── app.less
│   └── components/
│       └── ...
│
├── index.html
├── vite.config.js
└── package.json

After building, your dist/ folder might look like this:

dist/
├── assets/
│   ├── app-9f3a2b4d.js
│   ├── style-2c1f6a7e.css
│   └── vendor-1a2b3c4d.js
└── fonts/
    └── icon-font.woff

What’s in the dist/ folder:

assets/: contains final, optimized JS and CSS files:

  • app-xxxx.js – main application bundle
  • style-xxxx.css – compiled styles from .less, .scss, or .css files
  • vendor-xxxx.js – third-party libraries

fonts/: Includes static assets (like fonts) copied from the public/ folder

Key Notes:

  • The src/ folder and any source files (like app.less) are not included in the build.
  • All filenames are hashed for cache busting, ensuring browsers load the latest version after deployments.
  • This build is fully static and ready to be hosted on any static server (Netlify, Vercel, GitHub Pages, etc.)

How Absolute Paths Work in Vite

In Vite, an absolute path starts with a /. For example:

@icon-font-path: "/fonts/icon-font.woff";

The leading slash tells the browser (and Vite) to look in the root of the site.

Here’s how Vite resolves it:

In development (vite dev): Vite serves files in the public/ directory at the root level. So /fonts/icon-font.woff is resolved as public/fonts/icon-font.woff.

In production (vite build): Vite maintains this path, assuming that fonts/icon-font.woff will be at the root of the deployed dist/ folder.

This means that any asset placed in the public/ folder can be accessed directly using an absolute path that starts with /.

How Relative Paths Work in Vite

A relative path points to a file based on the current file’s location. For example:

@icon-font-path: "../fonts/icon-font.woff";

Relative paths work well when you're referencing files inside the src/ folder, as long as the file structure matches and Vite can bundle those files correctly.

However, issues can arise if the relative path doesn't correctly reflect the file system, especially if you're referencing something that lives outside of src/, such as in public/.

Real-life example: Why the relative path failed

Let’s look at a bug I encountered on a new project.
Here's the structure of the project:

project-folder/
│
├── public/
│    └── fonts/
│         └── icon-font.woff
│
├── src/
│    └── styles/
│         └── app.less
│

Originally, in app.less a relative path was used:

@icon-font-path: "../fonts/icon-font.woff";

This path worked fine in production and some environments, but when I ran the project locally with vite dev, the icon fonts didn’t load.

Why it failed locally:

  • The font folder was in the public/ directory.
  • Relative paths from within src/ can’t access files in public/.
  • So the file was simply not found.

Why it worked in production:

Vite was bundling everything, and the compiled CSS or JS file (e.g. app-9f3a2b4d.js) ended up inside the dist/assets/ folder. Meanwhile, the font file from the public/fonts/ directory was copied into dist/fonts/.

The resulting dist/ structure looked like this:

dist/
├── assets/
│   ├── app-9f3a2b4d.js
│   ├── style-2c1f6a7e.css
│   └── vendor-1a2b3c4d.js
└── fonts/
    └── icon-font.woff

The path used in the compiled CSS or JS was relative — and since those files were in dist/assets/, a path like ../fonts/icon-font.woff would still point correctly to dist/fonts/.

In other words, from assets/style-2c1f6a7e.css, the browser goes one level up (../) and finds the fonts/ folder right there at the root of dist/.

The path technically worked in production, but it was coincidental. The structure happened to align, not because the relative path was correct in source files, but because the build output ended up that way.

This is why the path didn’t work in vite dev, where files aren't organized the same way — which leads to the next point: using absolute paths ensures consistency across all environments.

The Fix:

Switching to an absolute path did the trick:

@icon-font-path: "/fonts/icon-font.woff";

Now Vite correctly served the font from public/, and the path worked in all environments: Dev, Prod, and even when viewing the site locally.

Conclusion

Path resolution in Vite is simple — but only once you know how it works.

Here’s the takeaway:

  • Use absolute paths (/...) for referencing files in public/
  • Use relative paths only for files inside src/
  • Always test both vite dev and vite build to ensure consistency

This small path fix saved me hours of debugging — I hope it saves you some too! Let me know if you’ve run into similar issues or have tips of your own.