The most surprising thing about Vite’s environment variable handling is that it doesn’t actually expose all your .env file variables to the browser.

Let’s see it in action. Imagine you have a .env file in your project root:

# .env
API_URL=https://api.example.com
PUBLIC_KEY=abcdef123456
PRIVATE_KEY=supersecretkey

And in your Vite configuration (vite.config.js):

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

export default defineConfig({
  plugins: [react()],
  // No explicit env loading here, Vite does it automatically
});

Now, in your React component:

// src/App.jsx
import React, { useEffect, useState } from 'react';

function App() {
  const [apiUrl, setApiUrl] = useState('');
  const [publicKey, setPublicKey] = useState('');
  const [privateKey, setPrivateKey] = useState('');

  useEffect(() => {
    setApiUrl(import.meta.env.VITE_API_URL || 'default-api');
    setPublicKey(import.meta.env.VITE_PUBLIC_KEY || 'no-public-key');
    setPrivateKey(import.meta.env.VITE_PRIVATE_KEY || 'no-private-key');
  }, []);

  return (
    <div>
      <h1>Vite Environment Variables</h1>
      <p>API URL: {apiUrl}</p>
      <p>Public Key: {publicKey}</p>
      <p>Private Key: {privateKey}</p>
    </div>
  );
}

export default App;

When you run npm run dev, you’ll see:

Vite Environment Variables
API URL: https://api.example.com
Public Key: abcdef123456
Private Key: no-private-key

Notice how PRIVATE_KEY is not exposed. This is by design. Vite automatically loads environment variables from .env, .env.local, .env.[mode], and .env.[mode].local files. However, to prevent accidental exposure of sensitive information to the client-side bundle, only variables prefixed with VITE_ are made available via import.meta.env.

The problem Vite solves is managing configuration that changes between environments (development, staging, production) and across different deployments without hardcoding values or committing secrets to version control. It provides a structured way to inject these values into your application during the build process.

Internally, Vite uses the dotenv package to load .env files. During the build, it scans for variables prefixed with VITE_ and substitutes them directly into your code. For example, import.meta.env.VITE_API_URL in your source code will be replaced with "https://api.example.com" in the final bundled JavaScript. This substitution happens before your code is sent to the browser, effectively inlining the values.

The exact levers you control are the .env file naming conventions and the VITE_ prefix. Vite prioritizes files in this order:

  1. .env (loads by default)
  2. .env.local (overrides .env, loaded by default)
  3. .env.[mode] (e.g., .env.development, .env.production)
  4. .env.local.[mode] (e.g., .env.local.development, .env.local.production) - highest priority

You can also define a specific mode using the NODE_ENV environment variable or the --mode flag when running Vite commands (e.g., vite build --mode production).

The system doesn’t automatically inject NODE_ENV into import.meta.env. Instead, import.meta.env.MODE will reflect the current mode Vite is running in. If you need NODE_ENV available on the client, you’ll typically need to define it yourself, often like VITE_NODE_ENV=$NODE_ENV in your .env files, which Vite will then expose as import.meta.env.VITE_NODE_ENV.

Understanding the VITE_ prefix is crucial. If you have a variable like DATABASE_URL in your .env file that you intend to use only on the server (e.g., in a Node.js SSR setup), you should not prefix it with VITE_. Vite will simply ignore it when building for the client. If you do prefix it with VITE_, it will be bundled into your client-side JavaScript, which is a significant security risk for sensitive credentials.

The next concept you’ll likely encounter is how to manage environment-specific variables more granularly, often using .env.development, .env.production, and their .local counterparts, and how these interact with the mode setting in Vite.

Want structured learning?

Take the full Vite course →