Common Resource Types

A detailed guide to the different file types you can bundle as resources in Tauri v2 with React and Vite, and how to access each one correctly at runtime

Tauri's resource system lets you ship files alongside your application that the webview frontend or the Rust backend can use at runtime. JSON configuration, custom fonts, local images, AI model weights, HTML templates — these are all files that would normally require a server or manual placement, but Tauri makes them part of the bundle.

This page catalogs the most common types you will bundle, with patterns for adding them to your configuration, loading them in React, and reading them from Rust. It assumes you understand the basics of adding resources (covered in Adding Resources). If you are new to the resources field in tauri.conf.json, review that page first.

Resource vs. Frontend Asset:

Resources are files that live outside the Vite build pipeline. They are not imported into your JavaScript bundle. If you need a static asset that will be handled by Vite (e.g., images inside src/assets), put it in your frontend directory instead. Resources are for files that must survive as separate physical files in the final application package, often because they are read by Rust, loaded at runtime with a URL, or too large to inline.

JSON and Configuration Files

Ship a settings.json or defaults.toml that your app can fetch without touching a network. The frontend can read it via the asset protocol, and the Rust backend can open it directly from the resource directory.

Bundling configuration files

Assume you have a file at resources/config/settings.json. Register it in tauri.conf.json:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/config/**"
    ]
  }
}

The glob pattern includes everything under resources/config/. After a build, the file ends up inside the application bundle and is accessible through the asset:// protocol.

Loading from the frontend

In your React code, use the asset protocol URL directly. The base is asset://localhost. For a file at resources/config/settings.json, the full URL becomes asset://localhost/config/settings.json — the top-level resources/ directory is stripped from the path.

src/App.tsx
import { useEffect, useState } from "react";
interface AppConfig {
  theme: string;
  apiBase: string;
}
export default function App() {
  const [config, setConfig] = useState<AppConfig | null>(null);
  useEffect(() => {
    fetch("asset://localhost/config/settings.json")
      .then((res) => res.json())
      .then(setConfig)
      .catch(console.error);
  }, []);
  if (!config) return <p>Loading configuration…</p>;
  return (
    <div style={{ background: config.theme === "dark" ? "#111" : "#fff" }}>
      <p>API base: {config.apiBase}</p>
    </div>
  );
}

The fetch call succeeds because Tauri registers a custom protocol handler. You don't need convertFileSrc for this case — the asset URL works directly in production and during tauri dev. During development, Tauri serves resource files from the same resources directory, so the same URL behaves identically.

Reading from the Rust backend

When you need the configuration file from a Rust command, use app.path().resource_dir() to construct the full path. This is safer than hard‑coding because the resource directory location changes between development and production.

src-tauri/src/main.rs
#[tauri::command]
fn read_settings(app: tauri::AppHandle) -> Result<String, String> {
    let resource_dir = app.path().resource_dir().map_err(|e| e.to_string())?;
    let settings_path = resource_dir.join("config/settings.json");
    std::fs::read_to_string(&settings_path)
        .map_err(|e| format!("Failed to read settings: {}", e))
}
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_settings])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The path config/settings.json mirrors the file’s location inside the resources/ directory — just as with the frontend URL, the resources/ prefix is not part of the resolved path.

Do not import JSON directly in the frontend:

Using import settings from "../resources/config/settings.json" will embed the JSON into your JavaScript bundle at build time. That is a static import handled by Vite, not a Tauri resource. If you need the file to remain external (so it can be changed without recompiling), always load it via fetch with the asset protocol.

Fonts

Custom fonts bundled as resources let you ship a complete design without an internet connection. The webview can load them through CSS @font-face declarations pointing to asset URLs.

Including font files

Place your .woff2 (or .ttf) fonts in a resource directory, e.g., resources/fonts/, and register them:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/fonts/**"
    ]
  }
}

Loading the font in CSS

Inside your global CSS file (imported by your React app), declare the font face using the asset protocol:

src/global.css
@font-face {
  font-family: "MyCustomFont";
  src: url("asset://localhost/fonts/my-font.woff2") format("woff2");
  font-weight: normal;
  font-style: normal;
}
body {
  font-family: "MyCustomFont", sans-serif;
}

The browser inside the webview resolves asset://localhost/fonts/my-font.woff2 to the physical file that Tauri serves. There are no CORS restrictions because the resource is loaded from the same origin as the page.

Relative paths break in production:

Do not write src: url("./fonts/my-font.woff2") or reference the file through a development‑server path like /fonts/my-font.woff2. During tauri dev those paths may work because Vite serves the file, but in a production build the webview runs from a custom origin where those relative paths no longer resolve. Always use the asset:// protocol for resources that are bundled via Tauri.

Verification:

After building and running your production bundle, open the developer tools (right‑click → Inspect) and check the Network tab. You should see the font request with the asset:// scheme returning a 200 status. If you see a 404, the file was not included in the resources — double‑check the glob pattern in tauri.conf.json.

Images

Images bundled as resources can be displayed in <img> tags, used as CSS backgrounds, or processed on the Rust side before being sent to the frontend.

Displaying an image in React

Given an image at resources/images/logo.png, add the resource pattern:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/images/**"
    ]
  }
}

Then use the asset URL directly in JSX:

src/components/Header.tsx
export default function Header() {
  return (
    <header>
      <img
        src="asset://localhost/images/logo.png"
        alt="App logo"
        style={{ height: 48 }}
      />
      <h1>My Tauri App</h1>
    </header>
  );
}

The image is served by the same protocol handler; no additional configuration is needed.

Processing images in Rust

Sometimes you want to manipulate an image — resize, convert formats, or extract metadata — before displaying it. The Rust backend can read the resource file directly, process it with a crate like image, and return a base64 data URL.

src-tauri/src/main.rs
use image::GenericImageView;
#[tauri::command]
fn get_processed_logo(app: tauri::AppHandle) -> Result<String, String> {
    let resource_dir = app.path().resource_dir().map_err(|e| e.to_string())?;
    let logo_path = resource_dir.join("images/logo.png");
    let img = image::open(&logo_path).map_err(|e| e.to_string())?;
    let (width, height) = img.dimensions();
    // Create a base64-encoded PNG data URL
    let mut buffer = std::io::Cursor::new(Vec::new());
    img.write_to(&mut buffer, image::ImageOutputFormat::Png)
        .map_err(|e| e.to_string())?;
    let base64 = base64::encode(buffer.into_inner());
    Ok(format!("data:image/png;base64,{}", base64))
}

The frontend can call this command and use the result as the src of an <img> element. This approach keeps the original file untouched and lets you do on‑the‑fly adjustments without bundling multiple pre‑processed copies.

Bundle size and image optimization:

Including many high‑resolution PNGs as resources will inflate your application’s download size. Consider converting images to modern formats (WebP, AVIF) or moving very large image collections to a sidecar or external download. Resources are always included in the bundle — there is no lazy loading from disk.

Audio and Video Files

Media files work the same way. Use the asset protocol in standard HTML5 tags, and the webview handles playback through the operating system’s codecs.

Example: local background music

Include a directory of audio files:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/audio/**"
    ]
  }
}

A React component that plays a background track:

src/components/AudioPlayer.tsx
import { useRef, useEffect } from "react";
export default function AudioPlayer() {
  const audioRef = useRef<HTMLAudioElement>(null);
  useEffect(() => {
    audioRef.current?.play().catch(() => {
      // Autoplay may be blocked; handle gracefully
    });
  }, []);
  return (
    <audio ref={audioRef} loop>
      <source src="asset://localhost/audio/ambient.mp3" type="audio/mpeg" />
      Your browser does not support the audio element.
    </audio>
  );
}

For video, use the <video> tag in exactly the same way.

Codec support depends on the platform:

The webview uses the OS’s native media stack. On Windows with WebView2, it uses Microsoft Edge’s codec support. On macOS it uses Safari’s. Always test your media files on all target platforms, especially if you rely on less common codecs like FLAC or HEVC.

Templates

HTML templates, Mustache files, or Markdown documents can be bundled and rendered at runtime. This is useful for generating reports, emails, or in‑app documentation where the template structure should remain editable without touching the compiled Rust code.

Bundling a simple HTML template

Place resources/templates/welcome.html:

<!DOCTYPE html>
<html>
<body>
  <h1>Welcome, {{username}}!</h1>
  <p>Your account was created on {{date}}.</p>
</body>
</html>

Configuration:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/templates/**"
    ]
  }
}

A Rust command can read the template, replace placeholders, and return the final HTML:

src-tauri/src/main.rs
#[tauri::command]
fn render_welcome(app: tauri::AppHandle, username: String) -> Result<String, String> {
    let resource_dir = app.path().resource_dir().map_err(|e| e.to_string())?;
    let template_path = resource_dir.join("templates/welcome.html");
    let template = std::fs::read_to_string(&template_path)
        .map_err(|e| format!("Could not read template: {}", e))?;
    let date = chrono::Local::now().format("%Y-%m-%d").to_string();
    let rendered = template
        .replace("{{username}}", &username)
        .replace("{{date}}", &date);
    Ok(rendered)
}

For more complex templating, use a crate like handlebars or tera. The pattern remains the same: resolve the path, read the file, and process it.

Loading a template from the frontend

If you don’t need Rust‑side processing, fetch the template as text and use a client‑side template engine (or simple string replacement):

src/components/WelcomeMessage.tsx
import { useEffect, useState } from "react";
export default function WelcomeMessage({ username }: { username: string }) {
  const [html, setHtml] = useState("");
  useEffect(() => {
    fetch("asset://localhost/templates/welcome.html")
      .then((res) => res.text())
      .then((template) => {
        const date = new Date().toISOString().split("T")[0];
        const rendered = template
          .replace("{{username}}", username)
          .replace("{{date}}", date);
        setHtml(rendered);
      });
  }, [username]);
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

Avoid dangerous outerHTML without sanitization:

If the template contains user‑supplied data, dangerouslySetInnerHTML can open XSS vectors. Sanitize the output with a library like DOMPurify before injecting it into the DOM, especially when the template source itself is under your control but the inserted values come from external input.

AI Model Files

Tauri applications that perform local machine‑learning inference need to ship model weights. ONNX files, TensorFlow Lite models, and other formats can be bundled as resources, making the model available offline without a download step.

Including a model

Assuming an ONNX model at resources/models/classifier.onnx:

src-tauri/tauri.conf.json
{
  "bundle": {
    "resources": [
      "resources/models/**"
    ]
  }
}

The Rust backend can load this model using a crate like ort (the Rust ONNX Runtime binding):

src-tauri/src/main.rs
use ort::session::Session;
#[tauri::command]
async fn run_inference(app: tauri::AppHandle, input: Vec<f32>) -> Result<Vec<f32>, String> {
    let resource_dir = app.path().resource_dir().map_err(|e| e.to_string())?;
    let model_path = resource_dir.join("models/classifier.onnx");
    let session = Session::builder()
        .map_err(|e| e.to_string())?
        .commit_from_file(model_path)
        .map_err(|e| e.to_string())?;
    // Build input tensor and run inference (details depend on model)
    let input_tensor = ort::value::Tensor::<f32>::from_array(([1, input.len() as i64], input.into_boxed_slice()))
        .map_err(|e| e.to_string())?;
    let outputs = session.run(ort::inputs![input_tensor].map_err(|e| e.to_string())?);
    // Extract result...
    Ok(vec![])
}

Large models and sidecars:

AI model files can easily exceed hundreds of megabytes. If your model is larger than a few megabytes, consider using Tauri’s sidecar functionality (external binaries and their data) instead of embedding it as a resource. Sidecar files are not baked into the binary; they sit next to the executable. This keeps the initial download size manageable and lets you update the model independently.

Common Pitfalls Across Resource Types

Beyond type‑specific mistakes, a few mistakes cut across every resource type. Spotting them early saves hours of debugging.

Resource paths in development vs. production:

During tauri dev, the resource directory is the resources/ folder in your project root. In a production build, resources are flattened into a single directory structure inside the app bundle. A file at resources/config/settings.json is accessible as config/settings.json in both environments, but only if you use the asset protocol or resolve_resource. Hard‑coding an absolute development path will break in production.

Forgetting to add the resource to tauri.conf.json:

A file sitting in resources/ is not automatically included. You must explicitly list it (or a glob pattern) under bundle > resources. If you miss this step, the file will be absent from the bundle, and all attempts to load it will fail with a 404 or a file‑not‑found error.

Verify with tauri build --debug:

Run tauri build --debug to create a development‑like bundle that still prints console messages. Check the asset protocol requests in the webview’s devtools. If all resources resolve correctly there, the production build will work as well.

Summary

Understanding these common resource types and how to handle them provides a solid foundation for adding arbitrary data to your Tauri application.