Navigate
Search topics across all sections
Colors & Color Spaces
Color in 3D is more nuanced than picking a hex code. Three.js works internally in Linear color space for physically correct lighting, then converts to sRGB for your monitor. Understanding this pipeline is the difference between colors that look "right" and colors that look washed out or unexpectedly dark.
You pick the perfect orange (#ff6600) in Figma. You paste it into your Three.js material. But the rendered color looks different — slightly darker, more saturated. You tweak the hex code back and forth but can never match the design. The issue isn't the color — it's the color space.
Material color doesn't match design mockup. Hex #ff6600 renders differently than in CSS.
Real-world
Think of color spaces as paint mixing systems.
RGB is like mixing red, green, and blue spotlights. Where they overlap, you get new colors. All three together make white. It's how screens work, but it's not how humans think about color.
HSL is more intuitive — like a paint store. Pick a hue from the color wheel (red, blue, green...), adjust saturation (how vivid vs gray), and set lightness (how bright vs dark). Much easier to think in.
The twist? Your monitor displays sRGB (gamma-corrected), but physics and lighting math work in Linear space. It's like the difference between raw and processed photos — both represent the same scene, but the math only works correctly on the raw version.
Your Color
Hex, RGB, HSL, or named
Linear Space
Lighting math happens here
Tone Mapping
Compress HDR range
sRGB Output
What your monitor shows
Hands-On: Working with Color
Three.js accepts colors in many formats. Let's explore each one and understand when the color space pipeline matters.
Step 1: Color input formats
// All of these work in R3F:
<meshStandardMaterial color="coral" /> // Named
<meshStandardMaterial color="#ff7f50" /> // Hex
<meshStandardMaterial color="rgb(255,127,80)" /> // RGB
<meshStandardMaterial color={0xff7f50} /> // Hex integer
// THREE.Color object for more control
const myColor = new THREE.Color("coral");
myColor.r // 1.0 (red channel, 0-1)
myColor.g // 0.498
myColor.b // 0.314R3F accepts any value that THREE.Color can parse: CSS color names, hex strings, RGB strings, hex integers, or a THREE.Color instance. Internally, all colors are stored as three floats (r, g, b) in the 0 to 1 range.
Step 2: HSL for intuitive color picking
// HSL is easier for humans
const color = new THREE.Color();
// setHSL uses 0-1 range for all values
color.setHSL(
0.6, // hue (0=red, 0.33=green, 0.66=blue)
0.8, // saturation (0=gray, 1=vivid)
0.5 // lightness (0=black, 0.5=pure, 1=white)
);
// Or parse a CSS HSL string
const color2 = new THREE.Color("hsl(216, 80%, 50%)");
// Get HSL back from any color
const hsl = {};
color.getHSL(hsl);
console.log(hsl.h, hsl.s, hsl.l);HSL is great for creating color variations programmatically. Want 10 different hues at the same brightness? Loop the hue value from 0 to 1 while keeping saturation and lightness constant.
Step 3: Understanding color spaces
// Three.js pipeline:
// 1. Input color (sRGB)
// 2. Convert to Linear for lighting math
// 3. Apply tone mapping
// 4. Convert back to sRGB for display
// For exact color matching (UI, branding):
<meshBasicMaterial
color="#ff6600"
toneMapped={false} // skip tone mapping
/>
// For physically correct rendering:
<meshStandardMaterial
color="#ff6600" // tone mapping applied
// Colors may shift slightly — that's correct!
/>Tone mapping compresses HDR values into displayable range. It makes scenes look more natural but shifts exact colors. Disable it on materials that need pixel-perfect color matching.
What you just learned
Three.js accepts colors as named strings, hex, RGB, HSL, or THREE.Color objects.
Internally, lighting calculations happen in Linear color space, then output converts to sRGB for display.
HSL (Hue, Saturation, Lightness) is more intuitive for generating color palettes and variations programmatically.
Use toneMapped={false} on materials that need exact color matching (UI, branding) to bypass tone mapping.
Question
If you mix two colors by averaging their RGB values (e.g., red + blue = purple), does it matter whether you do the math in sRGB or Linear space? What happens to the perceived brightness of the result?
Think about it...
You have a material with color '#808080' (50% gray). Why does it appear darker than expected when using MeshStandardMaterial compared to MeshBasicMaterial?
Hint: Think about what happens after the color is set but before it reaches your screen...
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
Pick a bright red — compare sRGB vs Linear
Try This!
Beginner
Set roughness to 0 — see color in reflections
Try This!
Beginner
Try named colors in the picker
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
// Colors look different than Figma/CSS!
// Three.js uses Linear internally
<meshStandardMaterial color="#ff6600" />
// This orange looks different than
// the same hex in CSS because of
// color space conversion// WRONG: new Color every frame!
function MyMesh() {
return (
<mesh>
<meshStandardMaterial
color={new THREE.Color(r, g, b)}
/>
</mesh>
);
}// WRONG: CSS-style percentages
const color = new THREE.Color();
color.setHSL(200, 80, 50);
// These values are way out of range!Best Practices
Use CSS Color Names for Prototyping
"coral", "royalblue", "gold" are faster to type than hex codes and easier to read. Switch to hex values for production color matching.
Use HSL for Palettes
Generate color variations by looping hue while keeping saturation and lightness constant. Much easier than manually picking harmonious RGB values.
Disable Tone Mapping for UI
For UI elements, brand colors, or anything that needs exact color matching, set toneMapped=false on the material. Let PBR materials keep tone mapping on.
Reuse Color Objects
If you animate colors, create a single THREE.Color with useMemo and mutate it in useFrame. Don't create new Color instances every frame — it creates garbage for the GC.