Assets
How static files like images, fonts, and data files reach the frontend in Tauri v2, with configuration and best practices
When you build a desktop application, the UI needs more than just HTML and JavaScript—it needs images, fonts, JSON data, stylesheets, and sometimes large media files. In Tauri v2, these are called assets, and getting them to load correctly across development and production requires understanding exactly how Tauri serves files.
This section covers what Tauri considers an asset, how static files from your frontend project end up inside the final binary, the role of the public directory, and the most reliable way to structure asset loading so it works the same in tauri dev and tauri build.
What Counts as an Asset
In Tauri v2, an asset is any file that the frontend loads at runtime—an image referenced in an <img> tag, a CSS file, a JSON data file fetched by JavaScript, or a font loaded via @font-face. These files come from your frontend framework's build output, which Tauri then embeds into the final application bundle.
The key piece of configuration is build.frontendDist. This field tells Tauri where to find the compiled frontend files — it lives in the build object. By default it points to ../dist, which is where Vite (or your bundler) outputs after running npm run build:
{
"build": {
"frontendDist": "../dist"
}
}
Any file inside that directory becomes an asset Tauri can serve. During development, however, the frontend is served by the Vite dev server at a URL like http://localhost:1420, not from the dist folder. This split—dev server vs. embedded files—is the root cause of most asset-loading confusion, and we'll return to it throughout this section.
How Tauri Serves Assets
Tauri uses a custom protocol to deliver static assets to the webview. In production, the protocol is https://asset.localhost (or just asset: in CSP directives). When your frontend code requests /logo.png, Tauri maps that path to the corresponding file inside the bundled frontend dist.
During development, the Vite dev server handles asset requests directly. That means your assets need to be reachable by Vite's static file serving rules, not by Tauri's custom protocol. The important takeaway is that the same relative path must work in both environments without hardcoding hostnames or protocols.
The security.assetProtocol configuration
Tauri lets you control which directories the asset protocol can read from through the security.assetProtocol field in tauri.conf.json. By default, the protocol is disabled and the scope is empty:
{
"app": {
"security": {
"assetProtocol": {
"enable": false,
"scope": []
}
}
}
}
If you need the protocol enabled—for example, if you're accessing assets from Rust code that reads from the asset protocol—you can enable it and restrict access to specific directories. The scope array takes paths relative to the src-tauri directory or absolute filesystem paths. For most applications built with Vite, you won't need to touch this; the default handling of the frontendDist folder is sufficient.
The Public Directory
Vite-based projects have a public directory at the project root. Any file placed there is copied directly into the build output (dist) without being processed by Vite. That makes it the simplest place to put static assets that don't need bundling—images, fonts, a manifest.json, or a large binary blob.
your-project/
├── public/
│ ├── logo.png
│ ├── fonts/
│ │ └── inter.woff2
│ └── data.json
├── src/
│ └── App.tsx
├── src-tauri/
│ └── tauri.conf.json
└── vite.config.ts
When you run npm run tauri build, Vite compiles the src code and copies everything in public into dist. Tauri then bundles that dist folder as the frontend assets. In npm run tauri dev, Vite's dev server serves the public directory directly. The path you use in your code is the same in both cases: just a leading slash followed by the filename.
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div>
{/* Logo served from the public directory */}
<img src="/logo.png" alt="App logo" width="64" height="64" />
<h1>Tauri + Vite + React</h1>
<button onClick={() => setCount(count + 1)}>count is {count}</button>
</div>
);
}
export default App;
The <img> tag uses src="/logo.png". In development, Vite returns the file from public/logo.png. In production, Tauri finds it inside the embedded dist folder. The same relative path works because Tauri's custom protocol resolves / to the root of the frontend dist.
Consistent path resolution:
Using root-relative paths like /logo.png is the most portable approach. It works identically in dev and production without any environment-specific logic.
Large files and Vite's inlining limit
Vite has a default asset inlining limit of 4 KB. Files smaller than this are inlined as base64 data URIs during build. That's usually fine, but for desktop apps with multi-megabyte assets—a splash screen video, a large JSON dataset—you may want to keep them as separate files so they aren't embedded into the JavaScript bundle.
To control this, set build.assetsInlineLimit in vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
// Files smaller than 10 KB are inlined; larger files stay as separate files
assetsInlineLimit: 10240,
},
});
For assets you always want as separate files, place them in the public directory—Vite never inlines those. For assets imported in JavaScript (like import logo from './logo.png'), the inlining limit applies.
Accessing Assets from Rust Code
Sometimes you need to read an asset from the Rust side—for example, to serve a file through a custom command or to inspect its contents at startup. Tauri provides an asset_resolver() method on the App and AppHandle structures.
use tauri::Manager;
#[tauri::command]
fn read_asset(app: tauri::AppHandle, path: String) -> Result<Vec<u8>, String> {
let resolver = app.asset_resolver();
let asset = resolver.get(&path).ok_or_else(|| format!("Asset not found: {}", path))?;
Ok(asset.bytes.to_vec())
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![read_asset])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
This lets you call read_asset from JavaScript to retrieve the raw bytes of any file that was bundled from the frontend dist.
Asset resolver is empty in development:
The asset resolver only contains files when running a production build (tauri build). During tauri dev, the frontend is served by the Vite dev server and Tauri's asset resolver returns an empty collection. If your code relies on asset_resolver().iter() or get() during development, it will fail silently. Always test asset-dependent Rust logic in a production build, or guard it with a check for the dev environment.
This behavior is a deliberate design choice: in dev mode, the webview loads assets directly from the dev server, so Tauri does not populate the asset resolver. If you need to iterate over assets during development, you'll need to structure your code to accept an empty resolver or use a different strategy for loading files.
Assets and the Content Security Policy
Tauri applies a Content Security Policy (CSP) to the webview, which restricts where resources can be loaded from. If you reference assets with the custom protocol, you must include the asset: source in your CSP. The default CSP generated by Tauri already allows asset: for img-src and style-src, but if you override it, make sure to keep those entries:
{
"app": {
"security": {
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost blob: data:",
"style-src": "'self' 'unsafe-inline' asset: https://asset.localhost"
}
}
}
}
If you accidentally remove asset: from your CSP, all images and fonts that rely on the custom protocol will be blocked, and you'll see errors in the developer console. The dangerousDisableAssetCspModification flag (default false) can be set to true to let Tauri automatically modify the CSP to allow assets, but it's safer to manage the CSP yourself.
CSP blocks missing asset sources silently in release builds:
In development, CSP violations are often visible in the browser console. In production, the webview may silently block resource loads without any obvious error—images just don't appear. After changing the CSP, always test a production build to confirm assets load correctly.
Asset Best Practices
Keep static files in public, not inside src
The asset best-practices page expands on these rules. Files placed in src that aren't explicitly imported into a module will be tree-shaken away by Vite and won't appear in the build output. The public directory is the designated place for files you want to survive the build untouched. Reserve src for code that gets bundled.
Use root-relative paths consistently
Never hardcode http://localhost:1420 or https://asset.localhost in your frontend code. Use a leading slash (like /images/icon.png) so the same path resolves correctly in both dev and production. If you need to construct a full URL programmatically, use window.location.origin as the base.
Watch out for file size with embedded webviews
Large assets increase both the install size and the memory footprint of your app because the webview loads them into memory. If you have multi-megabyte images or videos, consider compressing them, serving them on demand from a remote source, or using Tauri's resource system to load files from the filesystem rather than embedding them in the frontend dist.
Prefer relative imports for build-time optimization
When you import an asset in JavaScript (e.g., import logoSvg from './logo.svg'), Vite can hash the filename and apply optimizations like compression. This helps with cache busting and reduces the risk of stale assets. Use this approach for small, frequently updated assets. For files that change rarely or need to be accessed by absolute path (like a robots.txt equivalent), the public directory is fine.
Validate assets in production, not just dev
Because of the asset resolver difference and CSP behavior, testing asset loading only in tauri dev gives a false sense of security. As a rule, before shipping, run npm run tauri build and test the resulting executable to make sure every asset path works end-to-end.
Summary
Assets in Tauri v2 are the static files—images, fonts, data—that your frontend loads at runtime. They live in your bundler's output directory (dist for Vite), and Tauri embeds them into the final application using a custom protocol. The public directory in a Vite project is the easiest way to include files that don't need processing, and root-relative paths keep access consistent between dev and production.
The most common stumbling block is the asset resolver being empty during development. If your Rust code calls asset_resolver().get(), it will work in a production build but return nothing during tauri dev. Design your Rust logic accordingly, or accept that asset-dependent features are build-time-only. Combine that awareness with correct CSP configuration and thorough production testing, and your assets will load reliably across every target platform.
Understanding Assets
Learn what assets are in a Tauri application, how they differ from resources, and how the frontend build produces the files included in your final binary.
Static Assets
How images, fonts, CSS, videos, and other static files are embedded in a Tauri v2 app and served to the React frontend via the asset protocol.
Public Directory
How to place and reference static assets in a Tauri v2 application using the public directory of a Vite and React frontend
Asset Best Practices
Best practices for organizing, optimizing, and managing frontend assets in a Tauri v2 application using React and Vite