Navigate

Search topics across all sections

GitHub
Particles & Lines

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.

terminal
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

BasicSprite.tsxTSX
// 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

BillboardLabel.tsxTSX
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

HealthBar.tsxTSX
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.

1
Sprite appears as a white square instead of a texture
Forgot to load or set the texture map
Don't do this
TexturedSprite.tsxTSX
// No map set — renders as a solid white square
<sprite scale={[2, 2, 1]}>
  <spriteMaterial color="#ffffff" />
</sprite>
Without a map property, SpriteMaterial renders a flat solid color. Load your texture with useTexture and pass it as the map prop. Set transparent={true} if the image has alpha.
2
Billboard text renders behind 3D objects
Z-fighting or wrong render order
Don't do this
BillboardLabel.tsxTSX
// 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>
When a billboard overlaps with geometry, the depth buffer can hide it. Setting renderOrder to a higher number and disabling depthTest ensures the label always renders on top.
3
Hundreds of sprites cause performance issues
Each sprite is a separate draw call
Don't do this
ManySprites.tsxTSX
// 500 individual sprites = 500 draw calls
{Array.from({ length: 500 }).map((_, i) => (
  <sprite key={i} position={[...]}>
    <spriteMaterial map={texture} />
  </sprite>
))}
Each Sprite is a separate draw call. For a few dozen, that is fine. For hundreds, switch to a Points system which renders everything in a single call. Reserve sprites for individual labels and UI elements.

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.