Frontend frameworks
ICP hosts frontend applications in canisters: static files (HTML, CSS, JavaScript) deployed to the network and served with certified responses. Any framework that can produce a static build output works: React, Vue, Svelte, Next.js, and even game engines like Unity WebGL and Godot.
This guide shows you how to configure your framework’s build pipeline, wire up the ICP JavaScript SDK, and deploy it as a static site.
Prerequisites
Section titled “Prerequisites”- icp-cli installed:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - A backend canister deployed (or a static-only site with no backend)
- Familiarity with hosting a static site
The deployment model
Section titled “The deployment model”Every frontend framework integration follows the same pattern:
- Configure
icp.yamlto point at your framework’s build output directory - Optionally add a Vite plugin (
@icp-sdk/bindgen) to generate typed canister bindings at build time - Use
@icp-sdk/corein your app to read canister IDs and the root key at runtime from theic_envcookie the canister serves - Deploy with
icp deploy
The canister injects an ic_env cookie into every HTML response. This cookie carries the root key and any PUBLIC_CANISTER_ID:<name> environment variables you set: so your frontend never needs canister IDs baked into the build artifact.
React with Vite
Section titled “React with Vite”The hello-world template uses React with Vite. It demonstrates the fullstack: backend canister, auto-generated TypeScript bindings, and a React frontend that reads canister IDs at runtime.
icp.yaml
Section titled “icp.yaml”canisters: - name: frontend recipe: type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install - npm run generate --prefix app - npm run build dir: app/distThe build array runs before the canister is deployed and its files uploaded. npm run generate regenerates TypeScript bindings from the backend .did file; npm run build runs Vite.
vite.config.ts
Section titled “vite.config.ts”import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
// Change these values to match your local replica.// The `icp network start` command prints the root key;// the `icp deploy` command prints the backend canister ID.const IC_ROOT_KEY_HEX = "<IC_ROOT_KEY_HEX>";const BACKEND_CANISTER_ID = "<BACKEND_CANISTER_ID>";
export default defineConfig({ plugins: [ react(), icpBindgen({ didFile: "../../backend/backend.did", outDir: "./src/backend/api", }), ], server: { headers: { // Simulate the ic_env cookie that the canister injects in production. "Set-Cookie": `ic_env=${encodeURIComponent( `ic_root_key=${IC_ROOT_KEY_HEX}&PUBLIC_CANISTER_ID:backend=${BACKEND_CANISTER_ID}` )}; SameSite=Lax;`, }, proxy: { "/api": { target: "http://127.0.0.1:8000", changeOrigin: true, }, }, },});The icpBindgen Vite plugin regenerates TypeScript bindings whenever the .did file changes during development.
The server.headers block simulates the ic_env cookie during vite dev. In production, the canister injects this cookie automatically: your code reads it without any build-time environment variables.
Install the required packages:
npm install @icp-sdk/corenpm install -D @icp-sdk/bindgen @vitejs/plugin-reactReading canister IDs and the root key
Section titled “Reading canister IDs and the root key”import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";import { createActor } from "./backend/api/backend";
interface CanisterEnv { readonly "PUBLIC_CANISTER_ID:backend": string;}
// Reads from the ic_env cookie injected by the canister (production)// or the Set-Cookie header set in vite.config.ts (development).const canisterEnv = getCanisterEnv<CanisterEnv>();const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"];
const actor = createActor(canisterId, { agentOptions: { // In production, use the root key from the ic_env cookie. // In development (import.meta.env.DEV), fetch it from the local replica. rootKey: !import.meta.env.DEV ? canisterEnv.IC_ROOT_KEY : undefined, shouldFetchRootKey: import.meta.env.DEV, },});The createActor function is generated by @icp-sdk/bindgen from your .did file. It returns a fully typed actor. See the JS SDK docs for the full HttpAgent and Actor API.
SPA routing
Section titled “SPA routing”React apps use client-side routing. Without a fallback, refreshing on /about returns a 404. Add a _redirects file to your public/ directory so it ends up in dist/:
# Serve the app shell for any path with no matching file. The 200 makes this a# rewrite, so the URL the visitor typed stays in the address bar./* /index.html 200The canister adds no default headers, so declare the ones you want in a _headers file next to it:
/* X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin
/assets/* Cache-Control: public, max-age=31536000, immutableLink assets with absolute paths (/assets/app.js), never relative ones: under a /* rewrite a relative URL resolves against the client route, so the browser asks for /dashboard/assets/app.js and gets the HTML shell back.
See Redirects and rewrites and Custom headers for the full syntax, and Asset canister (legacy) if you are moving a project off .ic-assets.json5.
Vue with Vite
Section titled “Vue with Vite”Vue and Vite follow the same pattern as React. The only difference is the Vite plugin:
npm install @icp-sdk/corenpm install -D @icp-sdk/bindgen @vitejs/plugin-vueimport { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
export default defineConfig({ plugins: [ vue(), icpBindgen({ didFile: "../backend/backend.did", outDir: "./src/backend/api", }), ], server: { proxy: { "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, }, },});If your Vue app calls getCanisterEnv() to read canister IDs, add the same server.headers block from the React section to simulate the ic_env cookie during local development (otherwise getCanisterEnv() will throw because the cookie is absent. The icp.yaml configuration is the same as the React example) point dir at dist.
Authentication
Section titled “Authentication”Authentication with Internet Identity is framework-agnostic. The @icp-sdk/auth package works the same way in React, Vue, Svelte, and Next.js static export mode. See the Internet Identity guide for integration steps.
Svelte and SvelteKit
Section titled “Svelte and SvelteKit”For SvelteKit, you must configure static export mode before deploying. The canister serves static files and does not support server-side rendering.
SvelteKit with static adapter
Section titled “SvelteKit with static adapter”npm install -D @sveltejs/adapter-staticimport adapter from "@sveltejs/adapter-static";
export default { kit: { adapter: adapter({ pages: "build", assets: "build", fallback: "index.html", // enables SPA mode }), },};canisters: - name: frontend recipe: type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install - npm run build dir: buildFor Svelte (without SvelteKit), Vite is the standard build tool. The vite.config.js setup is the same as Vue: swap @vitejs/plugin-vue for @sveltejs/vite-plugin-svelte.
Next.js
Section titled “Next.js”Next.js requires static export mode. Server components, API routes, and getServerSideProps are not supported. The canister only serves static files.
Enable static export in your Next.js config:
const nextConfig = { output: "export",};
module.exports = nextConfig;This outputs static files to the out/ directory.
canisters: - name: frontend recipe: type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install - npm run build dir: outGame engines
Section titled “Game engines”Game engines that export HTML5 or WebGL builds can be deployed without a backend canister. The build output is pre-generated in the export step of the engine: icp.yaml just copies the files into place.
Unity WebGL
Section titled “Unity WebGL”Export your game from Unity Editor: File → Build Settings → WebGL → Build. This creates a folder with index.html, Build/, and TemplateData/.
canisters: - name: unity_webgl_template_assets recipe: type: "@dfinity/static-site@v0.3.3" configuration: dir: dist build: - mkdir -p dist - cp -r src/unity_webgl_template_assets/assets/* dist/ - cp -r src/unity_webgl_template_assets/src/* dist/The build commands copy the Unity WebGL export into dist/. Point dir at that directory.
See the Unity WebGL example for the full project structure.
Godot HTML5
Section titled “Godot HTML5”Export your game from Godot Editor: Project → Export → HTML5 → Export Project. This creates an index.html and supporting files.
canisters: - name: godot_html5_assets recipe: type: "@dfinity/static-site@v0.3.3" configuration: dir: dist build: - mkdir -p dist - cp -r src/godot_html5_assets/assets/* dist/ - cp -r src/godot_html5_assets/src/* dist/See the Godot HTML5 example for the full project structure.
Deploying game builds
Section titled “Deploying game builds”Both game engine templates deploy with standard icp-cli commands:
# Start local networkicp network start -d
# Deploy the frontend canistericp deploy
# Access your game locally# http://<canister-id>.localhost:8000No Vite plugin or JS SDK integration is needed for game builds. The canister serves the pre-built HTML and JavaScript files directly.
Static sites
Section titled “Static sites”For sites with no backend canister (portfolios, landing pages, documentation):
canisters: - name: frontend recipe: type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install - npm run build dir: distNo JS SDK integration is needed. The canister serves your files, and you configure headers and caching in _headers.
See the React hosting example for a minimal static frontend without a backend canister.
Deploy
Section titled “Deploy”# Start local networkicp network start -d
# Deploy all canistersicp deploy
# Deploy to mainneticp deploy -e icAfter deployment, the URL depends on your canister ID:
| Environment | URL |
|---|---|
| Local | http://<canister-id>.localhost:8000 |
| Mainnet | https://<canister-id>.icp.net |
Get your canister ID with:
icp canister settings show frontend -iNext steps
Section titled “Next steps”- Hosting a static site: headers, redirects, clean URLs, and access protection
- Internet Identity: add authentication to your frontend
- Project structure: explore the hello-world template with React, Vite, and a Motoko backend