Skip to main content
Supported on
Snapchat

Build your first Web Lens

This guide takes you from an empty folder to a Web Lens running in Snapchat. It uses PlayCanvas for rendering and Vite for building, the stack behind the first Web Lens games. Any web stack that produces a static build follows the same steps.

Prerequisites​

  • Node.js 20 or later.
  • A Snapchat account with access to My Lenses.
  • A phone, to test the build in its browser, with Snapchat installed to open the Lens once it's approved.

Set up your AI coding agent​

If you build with an AI coding agent, install the PlayCanvas Skills before you start. They teach the agent PlayCanvas conventions, scene assembly, lighting, heads-up display (HUD) structure, game state, and effects, so it writes code that fits the Engine's conventions.

npx skills add playcanvas/skills

The skills work with Claude Code, Codex, and Cursor. The PlayCanvas Skills repository lists all sixteen. This step is optional; the rest of this page works without it.

Create the project​

mkdir my-web-lens
cd my-web-lens
npm init -y
npm install playcanvas
npm install --save-dev vite

In package.json, set "type": "module" so Node loads the Vite config as an ES module, and add two scripts:

{
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
}
}

Configure Vite for relative paths​

A Web Lens loads from a location you don't control, so every asset URL in the build must be relative. Set base to ./:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
base: './',
build: {
target: 'es2020',
},
});

Without base: './', Vite emits absolute /assets/... paths and the Lens shows a blank screen.

Add the page​

Create index.html at the project root. It's the entry point Snapchat loads.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title>My Web Lens</title>
<style>
html,
body {
margin: 0;
height: 100%;
overflow: hidden;
background: #1b1b3a;
touch-action: none;
overscroll-behavior: none;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="app"></canvas>
<script type="module" src="./src/main.js"></script>
</body>
</html>

viewport-fit=cover lets the page extend under the system bars so you can honor the safe-area insets yourself. The background color matches the scene's clear color, so the first paint isn't a white flash. Runtime environment explains both.

Render something​

Create src/main.js with a minimal PlayCanvas scene: a camera, a light, and a spinning box.

import {
Application,
Color,
Entity,
FILLMODE_FILL_WINDOW,
RESOLUTION_AUTO,
} from 'playcanvas';

const canvas = document.getElementById('app');
const app = new Application(canvas);

app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
app.setCanvasResolution(RESOLUTION_AUTO);
app.graphicsDevice.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
window.addEventListener('resize', () => app.resizeCanvas());

const camera = new Entity('camera');
camera.addComponent('camera', { clearColor: new Color(0.1, 0.1, 0.23) });
camera.setPosition(0, 1.5, 6);
camera.lookAt(0, 0, 0);
app.root.addChild(camera);

const light = new Entity('light');
light.addComponent('light', { type: 'directional' });
light.setEulerAngles(45, 30, 0);
app.root.addChild(light);

const box = new Entity('box');
box.addComponent('render', { type: 'box' });
app.root.addChild(box);

app.on('update', (dt) => box.rotate(0, 40 * dt, 0));
app.start();

Import the names you use rather than import * as pc. Vite can then drop the engine modules you never reference, which keeps the zip small.

Run npm run dev and open the URL Vite prints. A rotating box appears. Narrow the window to a portrait shape, since that's how the Lens runs.

Build​

npm run build

Vite writes the site to dist/. Open dist/index.html and check that its script and style sheet references start with ./. It should be a few kilobytes; Snapchat rejects an index.html larger than 1 MB, so never inline your bundle into it.

Test on a phone​

You can't open a Web Lens inside Snapchat until it passes review, so test the build in your phone's browser first. Serve dist/ on your local network:

npx vite preview --host

Open the network URL Vite prints on your phone, in portrait. This checks layout, pointer input, and performance on real hardware. It can't show Snapchat's controls over your content, so also simulate a large top inset as described in Runtime environment.

Zip the build output​

Create a zip whose root is the contents of dist/, not the dist/ folder itself: index.html must sit at the top level of the zip.

On macOS or Linux:

cd dist && zip -r ../my-web-lens.zip . && cd ..

On Windows, open dist in Explorer, select everything inside it, right-click, and choose Compress to ZIP file. Compressing the dist folder itself would put a dist/ folder at the root of the zip instead. Don't script the zip with PowerShell's Compress-Archive: it writes backslashes as path separators, which breaks every relative path when Snapchat unpacks the zip. For automation, use the Node script in Platform requirements.

Check the zip before uploading:

unzip -l my-web-lens.zip

The listing should start with index.html, and every nested path should use /.

Upload​

  1. Sign in to My Lenses.
  2. Create a new Web Lens and upload my-web-lens.zip.
  3. Add a Lens icon and a preview video. Publishing has the specifications.
  4. Submit for review.

After approval, your Lens is live in Snapchat. Find it by name or by scanning its Snapcode.

Next steps​

Was this page helpful?
Yes
No