Navigate
Search topics across all sections
Sprites & Billboards
Sprites are flat images that always face the camera — no matter which direction you look from. They are the go-to tool for health bars, floating labels, lightweight particle effects, and any 2D element that needs to live in 3D space without looking weird from the side.
You add a floating health bar above a character using a regular plane mesh. It looks great from the front, but when the camera orbits to the side, the health bar turns into a paper-thin line and disappears. It is a flat surface viewed edge-on.
Health bar label is invisible from most camera angles. Looks like a thin line from the side.
Real-world
Think of sprites as pop-up signs in a museum. Wherever you walk in the room, the sign rotates to face you. You never see it from the side. You never see its edge. It always presents its flat face directly toward your eyes.
A regular plane mesh is like a painting on a wall — it stays fixed and you can walk around to see its edge. A billboard is like a lazy Susan sign that spins to track you.
Three.js gives you two tools for this. The built-in Sprite object always faces the camera automatically. And drei's Billboard component wraps any JSX children so they always face the camera too — even complex layouts with text and shapes.
Choosing the Right Approach
Sprites and Billboards solve the same problem differently.
Sprite
Built-in Three.js — single textured quad
SpriteMaterial
Texture map, color, opacity, blending
Billboard
drei wrapper — any JSX children face camera
HTML Overlay
Alternative: CSS-based, always on top
Building Billboarded Labels Step by Step
Let us add floating labels above game objects that always face the camera.
Step 1 -- A basic Sprite
// A sprite is a camera-facing quad
<sprite position={[0, 2, 0]} scale={[1, 0.4, 1]}>
<spriteMaterial
color="#ff6b6b"
transparent
opacity={0.9}
/>
</sprite>The sprite is always a flat quad facing the camera. The scale controls its size — [width, height, 1]. No matter how you orbit, you always see the front face.
Step 2 -- Billboard with Text
import { Billboard, Text } from "@react-three/drei";
<Billboard position={[0, 2, 0]}>
<Text fontSize={0.3} color="white">
Player 1
</Text>
</Billboard>Drei's Billboard wraps any children and makes the whole group face the camera. Unlike Sprite, you can put complex JSX inside — text, planes, even other meshes.
Step 3 -- A health bar with background
function HealthBar({ health }: { health: number }) {
return (
<Billboard position={[0, 2.2, 0]}>
{/* Background bar */}
<mesh scale={[1.2, 0.15, 1]}>
<planeGeometry />
<meshBasicMaterial
color="#333"
transparent
opacity={0.7}
/>
</mesh>
{/* Health fill */}
<mesh
scale={[1.2 * health, 0.12, 1]}
position={[(health - 1) * 0.6, 0, 0.01]}
>
<planeGeometry />
<meshBasicMaterial color="#22c55e" />
</mesh>
</Billboard>
);
}Two planes layered on each other inside a Billboard. The green bar scales based on the health value (0 to 1). The position offset keeps it left-aligned as it shrinks.
What you just learned
Sprites are built-in Three.js objects that always face the camera — rendered as a textured quad.
drei's Billboard component wraps any JSX children to make them face the camera, enabling complex UI layouts in 3D.
SpriteMaterial supports texture maps, transparency, and blending for particle-like effects.
For many sprite-like particles, switch to Points for a single draw call instead of hundreds of individual sprites.
Question
A sprite always faces the camera, which means it rotates as you orbit. But what if you want a health bar that only rotates on the Y-axis (left-right) and stays level on the X-axis (never tilts up or down)? How would you implement that?
Think about it...
You have 400 floating sparkle particles in your scene. Each is a sprite with a glow texture. Performance is terrible. What should you do?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Set opacity to 0.3 — ghostly labels
Try This!
Beginner
Toggle sparkles — remove particles
Try This!
Beginner
Set scale to 2 — giant signs
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
// No map set — renders as a solid white square
<sprite scale={[2, 2, 1]}>
<spriteMaterial color="#ffffff" />
</sprite>// Label hides behind the mesh it labels
<mesh position={[0, 0, 0]}>
<sphereGeometry />
<meshStandardMaterial />
</mesh>
<Billboard position={[0, 1.5, 0]}>
<Text>Label</Text>
</Billboard>// 500 individual sprites = 500 draw calls
{Array.from({ length: 500 }).map((_, i) => (
<sprite key={i} position={[...]}>
<spriteMaterial map={texture} />
</sprite>
))}Best Practices
Sprites for few, Points for many
Use Sprites for a handful of individual labels. Switch to Points when you have dozens or hundreds of similar particles. The draw call savings are dramatic.
Set depthWrite false on transparent sprites
Transparent sprites with depthWrite on will occlude objects behind them with their invisible parts. Always set depthWrite={false} when using transparency.
Billboard for complex UI
When you need more than a single texture — text, shapes, progress bars — use drei's Billboard. It wraps any JSX so the whole group faces the camera.
Test from multiple angles
Billboarding bugs are only visible from certain camera angles. Always orbit around your scene to verify labels and sprites behave correctly from every direction.