Learn how to easily add augmented reality stickers to your web app with our step-by-step guide. Enhance user experience today!

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
The Business Value of AR Stickers
Before diving into the technical implementation, let's understand why AR stickers matter. AR stickers allow users to place virtual objects in their real-world environment through their device camera, creating interactive and personalized experiences that can:
The Technical Landscape
Implementing AR on the web has historically been challenging, but thanks to WebXR and supporting libraries, it's now accessible without requiring users to download a separate app. Here's what you need to know:
Foundation Technologies
For AR stickers specifically, we'll need:
First, let's establish our project and dependencies:
// Initialize a new project
npm init -y
// Install required dependencies
npm install three @ar-js-org/ar.js-threejs
Project Structure
A clean structure will make your AR feature more maintainable:
/your-web-app
/public
/assets
/stickers // Your 3D models and textures go here
/markers // Reference images for image tracking
/src
/components
ARView.js // The AR viewer component
StickerMenu.js // UI for selecting stickers
/services
ar-service.js // Core AR functionality
/utils
gesture-handler.js // Handle user interactions
Let's implement the core AR functionality:
// ar-service.js
import * as THREE from 'three';
import { ARjs } from '@ar-js-org/ar.js-threejs';
export class ARService {
constructor(containerId) {
// Get the container element
this.container = document.getElementById(containerId);
this.scene = null;
this.camera = null;
this.renderer = null;
this.arToolkitSource = null;
this.arToolkitContext = null;
this.stickers = []; // Will hold all active stickers
}
initialize() {
// Create the Three.js scene
this.scene = new THREE.Scene();
// Set up camera
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000);
// Configure renderer
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true // Transparent background
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.container.appendChild(this.renderer.domElement);
// Set up AR.js
this.arToolkitSource = new ARjs.Source({
sourceType: 'webcam',
});
// Handle resize to maintain proper camera aspect
this.arToolkitSource.init(() => {
this.onResize();
});
// Set up AR context for marker tracking
this.arToolkitContext = new ARjs.Context({
cameraParametersUrl: 'assets/camera_para.dat',
detectionMode: 'mono',
});
// Initialize the AR context
this.arToolkitContext.init(() => {
// Copy projection matrix to camera
this.camera.projectionMatrix.copy(this.arToolkitContext.getProjectionMatrix());
});
// Add some light to the scene
const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1);
this.scene.add(light);
// Start the rendering loop
this.animate();
}
onResize() {
this.arToolkitSource.onResizeElement();
this.arToolkitSource.copyElementSizeTo(this.renderer.domElement);
if (this.arToolkitContext.arController !== null) {
this.arToolkitSource.copyElementSizeTo(this.arToolkitContext.arController.canvas);
}
}
animate() {
requestAnimationFrame(this.animate.bind(this));
if (this.arToolkitSource.ready) {
this.arToolkitContext.update(this.arToolkitSource.domElement);
this.scene.visible = true;
}
this.renderer.render(this.scene, this.camera);
}
}
Now, let's add the functionality to load and place stickers:
// Extend the ARService class with sticker management
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
// Add these methods to the ARService class
loadSticker(stickerPath, scale = 1.0) {
return new Promise((resolve, reject) => {
const loader = new GLTFLoader();
loader.load(
stickerPath,
(gltf) => {
const model = gltf.scene;
// Scale the model appropriately for AR
model.scale.set(scale, scale, scale);
// Center the model
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
resolve(model);
},
(xhr) => {
console.log(`${(xhr.loaded / xhr.total) * 100}% loaded`);
},
(error) => {
console.error('Error loading sticker:', error);
reject(error);
}
);
});
}
addStickerToScene(stickerModel, position = { x: 0, y: 0, z: -0.5 }) {
// Clone the model so we can have multiple instances
const sticker = stickerModel.clone();
// Position relative to the camera initially
sticker.position.set(position.x, position.y, position.z);
// Add to our scene
this.scene.add(sticker);
// Keep track of it
this.stickers.push(sticker);
return sticker;
}
// Method to place a sticker in the real world based on a surface detection
placeSticker(stickerModel) {
// Create a marker for surface detection
const markerRoot = new THREE.Group();
this.scene.add(markerRoot);
// Create a new AR.js marker controller
const markerControls = new ARjs.MarkerControls(this.arToolkitContext, markerRoot, {
type: 'pattern',
patternUrl: 'assets/markers/surface-marker.patt',
});
// Add the sticker to the marker root
const sticker = stickerModel.clone();
markerRoot.add(sticker);
// Keep track of the sticker
this.stickers.push(sticker);
return sticker;
}
Let's make our stickers interactive with gestures:
// gesture-handler.js
export class GestureHandler {
constructor(renderer, camera, scene) {
this.renderer = renderer;
this.camera = camera;
this.scene = scene;
this.selectedSticker = null;
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
// Starting position for gestures
this.startPoint = { x: 0, y: 0 };
this.lastPoint = { x: 0, y: 0 };
// Flags for gesture state
this.isDragging = false;
this.isPinching = false;
this.startDistance = 0;
// Bind event listeners
this.bindEvents();
}
bindEvents() {
// Touch events for mobile
this.renderer.domElement.addEventListener('touchstart', this.onTouchStart.bind(this));
this.renderer.domElement.addEventListener('touchmove', this.onTouchMove.bind(this));
this.renderer.domElement.addEventListener('touchend', this.onTouchEnd.bind(this));
// Mouse events for desktop
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
}
onTouchStart(event) {
event.preventDefault();
if (event.touches.length === 1) {
// Single touch - potential drag
this.startPoint.x = event.touches[0].pageX;
this.startPoint.y = event.touches[0].pageY;
this.lastPoint.x = this.startPoint.x;
this.lastPoint.y = this.startPoint.y;
// Check if user touched a sticker
this.selectStickerAtPoint(this.startPoint.x, this.startPoint.y);
this.isDragging = !!this.selectedSticker;
}
else if (event.touches.length === 2) {
// Two fingers - potential pinch/zoom
this.isPinching = true;
this.startDistance = Math.hypot(
event.touches[0].pageX - event.touches[1].pageX,
event.touches[0].pageY - event.touches[1].pageY
);
}
}
onTouchMove(event) {
event.preventDefault();
if (this.isDragging && event.touches.length === 1) {
// Handle drag
const currentX = event.touches[0].pageX;
const currentY = event.touches[0].pageY;
this.moveSelectedSticker(currentX - this.lastPoint.x, currentY - this.lastPoint.y);
this.lastPoint.x = currentX;
this.lastPoint.y = currentY;
}
else if (this.isPinching && event.touches.length === 2) {
// Handle pinch/zoom for scaling
const currentDistance = Math.hypot(
event.touches[0].pageX - event.touches[1].pageX,
event.touches[0].pageY - event.touches[1].pageY
);
if (this.selectedSticker) {
const scale = currentDistance / this.startDistance;
this.scaleSelectedSticker(scale);
this.startDistance = currentDistance;
}
}
}
onTouchEnd(event) {
this.isDragging = false;
this.isPinching = false;
}
// Similar implementations for mouse events...
selectStickerAtPoint(x, y) {
// Convert to normalized device coordinates
this.mouse.x = (x / window.innerWidth) * 2 - 1;
this.mouse.y = -(y / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
// Find intersections with stickers
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
// Find the first intersected object that is a sticker
for (let i = 0; i < intersects.length; i++) {
// You'll need logic here to determine if the object is a sticker
// For example, checking a custom property or tag
this.selectedSticker = intersects[i].object;
break;
}
} else {
this.selectedSticker = null;
}
}
moveSelectedSticker(deltaX, deltaY) {
if (!this.selectedSticker) return;
// Convert screen movement to world space movement
// This is a simplified approach - real implementation would need
// to account for perspective and camera orientation
const movementSpeed = 0.01;
this.selectedSticker.position.x += deltaX * movementSpeed;
this.selectedSticker.position.y -= deltaY * movementSpeed; // Note the inversion for Y
}
scaleSelectedSticker(scaleFactor) {
if (!this.selectedSticker) return;
// Apply scaling, but limit to reasonable bounds
const newScale = this.selectedSticker.scale.x * scaleFactor;
if (newScale > 0.1 && newScale < 5.0) {
this.selectedSticker.scale.set(newScale, newScale, newScale);
}
}
}
Now we need a UI to let users select and place stickers:
// StickerMenu.js - React component example
import React, { useState, useEffect } from 'react';
const StickerMenu = ({ onStickerSelected }) => {
const [stickers, setStickers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch available stickers
fetch('/api/stickers')
.then(response => response.json())
.then(data => {
setStickers(data);
setLoading(false);
})
.catch(error => {
console.error('Error loading stickers:', error);
setLoading(false);
});
}, []);
return (
<div className="sticker-menu">
<h3>Choose a Sticker</h3>
{loading ? (
<div className="loading">Loading stickers...</div>
) : (
<div className="sticker-grid">
{stickers.map(sticker => (
<div
key={sticker.id}
className="sticker-item"
onClick={() => onStickerSelected(sticker)}
>
<img src={sticker.thumbnail} alt={sticker.name} />
<span>{sticker.name}</span>
</div>
))}
</div>
)}
</div>
);
};
export default StickerMenu;
Let's create the main AR component that brings everything together:
// ARView.js - React component example
import React, { useEffect, useRef, useState } from 'react';
import { ARService } from '../services/ar-service';
import { GestureHandler } from '../utils/gesture-handler';
import StickerMenu from './StickerMenu';
const ARView = () => {
const containerRef = useRef(null);
const [arService, setArService] = useState(null);
const [gestureHandler, setGestureHandler] = useState(null);
const [showMenu, setShowMenu] = useState(true);
const [loadedStickers, setLoadedStickers] = useState({});
// Initialize AR on component mount
useEffect(() => {
if (!containerRef.current) return;
// Check if browser supports WebXR
if (!navigator.xr) {
alert("Your browser doesn't support WebXR. Please try a compatible browser like Chrome or Firefox.");
return;
}
// Initialize AR
const ar = new ARService(containerRef.current.id);
ar.initialize();
setArService(ar);
// Initialize gesture handler
const gestures = new GestureHandler(ar.renderer, ar.camera, ar.scene);
setGestureHandler(gestures);
// Handle browser resize
const handleResize = () => ar.onResize();
window.addEventListener('resize', handleResize);
// Cleanup on unmount
return () => {
window.removeEventListener('resize', handleResize);
// Additional cleanup as needed
};
}, []);
const handleStickerSelected = async (sticker) => {
if (!arService) return;
setShowMenu(false);
// Load the sticker if not already loaded
if (!loadedStickers[sticker.id]) {
try {
const model = await arService.loadSticker(sticker.modelPath, sticker.scale || 1.0);
setLoadedStickers(prev => ({
...prev,
[sticker.id]: model
}));
// Place the sticker in AR space
arService.addStickerToScene(model);
} catch (error) {
console.error('Failed to load sticker:', error);
alert('Sorry, there was an error loading this sticker. Please try another one.');
}
} else {
// Use the already loaded model
arService.addStickerToScene(loadedStickers[sticker.id]);
}
};
return (
<div className="ar-container">
<div id="ar-scene" ref={containerRef} className="ar-scene"></div>
{showMenu ? (
<StickerMenu onStickerSelected={handleStickerSelected} />
) : (
<div className="ar-controls">
<button onClick={() => setShowMenu(true)}>Choose Another Sticker</button>
<button onClick={() => arService && arService.clearLastSticker()}>Remove Last Sticker</button>
</div>
)}
</div>
);
};
export default ARView;
To ensure your AR stickers perform well in production, implement these optimizations:
Model Optimization
// In your AR service
optimizeModels() {
// Use draco compression for models
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/assets/draco/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
// Use this enhanced loader for your models
return gltfLoader;
}
Progressive Loading
// Add to your AR service
preloadCommonStickers() {
// Preload most commonly used stickers in the background
const commonStickerIds = ['sticker1', 'sticker2', 'sticker3'];
commonStickerIds.forEach(id => {
fetch(`/api/stickers/${id}`)
.then(response => response.json())
.then(sticker => {
// Load in background with low priority
this.loadSticker(sticker.modelPath, sticker.scale)
.then(model => {
// Store for later use
this.preloadedStickers[id] = model;
});
});
});
}
Device Testing Strategy
WebXR support varies across browsers and devices. Implement a robust detection and fallback system:
// Browser compatibility detection
checkCompatibility() {
if (navigator.xr) {
// Check if AR is supported
navigator.xr.isSessionSupported('immersive-ar')
.then(supported => {
if (supported) {
this.initializeAR();
} else {
this.showARUnsupportedMessage();
}
});
} else {
// WebXR not supported at all
this.showWebXRUnsupportedMessage();
}
}
// Implement appropriate fallback UI methods
showARUnsupportedMessage() {
// Show UI for devices that support WebXR but not AR
}
showWebXRUnsupportedMessage() {
// Show UI for devices that don't support WebXR at all
}
Performance Considerations
User Experience Best Practices
Business Implementation Strategy
Start with a phased rollout:
ROI Measurement
Track these metrics to evaluate success:
AR stickers represent more than just a fun feature—they're an engagement tool that creates memorable experiences for users. The implementation we've covered strikes a balance between technical robustness and user experience, allowing you to start with a core offering and expand based on user feedback.
The code we've explored will get you 80% of the way there. The remaining 20% will involve integrating with your specific frontend framework, setting up proper asset management, and fine-tuning the experience for your particular audience.
Remember that WebXR is still evolving—staying current with browser updates and maintaining fallback options will ensure your AR stickers remain accessible to the widest possible audience.
Explore the top 3 practical and engaging AR sticker use cases for your web app.
AR stickers allow shoppers to digitally place products in their environment before purchasing. A customer can visualize how furniture fits in their space, how clothing looks on their body, or how accessories complement existing items—all through their smartphone camera view.
AR stickers can attach contextual information to physical locations or objects. Think restaurant ratings appearing above storefronts, historical information overlaid on landmarks, or maintenance instructions hovering over machine parts—creating an intuitive digital layer over the physical world.
AR stickers transform static marketing materials into interactive experiences. Product packaging, billboards, or print advertisements become gateways to animations, games, or exclusive content when viewed through a smartphone camera.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â