Vite treats static assets like images and fonts differently depending on where you place them, and understanding this distinction is key to avoiding unexpected build failures or missing files.

Let’s see Vite in action. Imagine you’re building a simple React app.

// src/App.jsx
import React from 'react';
import logo from './logo.png'; // Import an image from src/
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.jsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        <img src="/favicon.ico" alt="favicon" /> {/* Image from public/ */}
      </header>
    </div>
  );
}

export default App;
/* src/App.css */
.App {
  text-align: center;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .App-logo {
    animation: none;
  }
}

.App-link {
  color: #61dafb;
}

And you have these files in your project:

my-vite-app/
├── public/
│   └── favicon.ico
├── src/
│   ├── App.css
│   ├── App.jsx
│   └── logo.png
└── index.html

When you run npm run dev (or yarn dev), Vite starts a development server. If you run npm run build (or yarn build), Vite will create a production-ready build in a dist folder.

The core of Vite’s asset handling lies in two main locations: files imported within your JavaScript/TypeScript code (like src/logo.png) and files placed in the public/ directory.

When you import an asset like import logo from './logo.png';, Vite’s build process (powered by Rollup) kicks in. It analyzes this import, processes the logo.png file (potentially optimizing it, hashing its name for cache-busting), and then embeds the resulting URL into your JavaScript bundle. The src attribute of your <img> tag will dynamically resolve to something like /assets/logo.a1b2c3d4.png. This ensures that your assets are versioned and served efficiently.

On the other hand, files in the public/ directory are treated as static assets that are copied directly to the root of your build output. When you reference /favicon.ico in your index.html or in your JavaScript, Vite serves it directly from the root. During development, Vite serves these files from the public/ directory. In a production build, these files are copied to the root of the dist folder. So, public/favicon.ico becomes dist/favicon.ico.

This distinction is crucial. If you try to import a file from public/ directly into your JS/TS (e.g., import favicon from './public/favicon.ico'), Vite will likely throw an error or not process it as you expect because it’s not designed to be bundled that way. These files are meant to be served at their original paths.

You can configure where Vite looks for static assets and where it outputs them. The build.outDir option in your vite.config.js controls the output directory (defaults to dist). The publicDir option controls the directory that’s copied to the root of the output directory (defaults to public).

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'build', // Output to 'build' instead of 'dist'
    publicDir: 'static', // Copy files from 'static/' to the root of the output directory
  },
});

If you place favicon.ico in a static/ directory and change publicDir to 'static', then in your build, static/favicon.ico will be copied to build/favicon.ico.

The most surprising thing about Vite’s asset handling is how seamlessly it integrates with modern JavaScript module syntax for assets within src/ while maintaining a simple copy-and-serve mechanism for the public/ directory, all driven by its lightning-fast dev server and optimized Rollup build.

The default behavior for publicDir is to copy its contents to the root of the build output. This means if you have public/assets/images/logo.png, it will end up as dist/assets/images/logo.png. However, if you refer to it as /assets/images/logo.png in your index.html, Vite will ensure that path is correctly resolved and served. This can lead to confusion if you expect public/ files to be automatically relative to your index.html when they are actually copied to the root of the output directory.

Next, you’ll want to explore how Vite handles CSS and preprocessors like Sass or Less.

Want structured learning?

Take the full Vite course →