HSL vs HSV Color Models

阅读时间 8 分钟更新于 2026-06-25
📘 本文内容为英文原文,提供最准确的技术信息。中文解读和实操指南正在完善中。你也可以使用页面顶部的翻译工具。

Here's the thing about HSL and HSV: they both describe color with hue, saturation, and a third axis — but they define "saturation" and that third axis differently enough to confuse anyone switching between tools. Photoshop uses HSB (same as HSV). CSS uses HSL. Figma's color picker shows HSB but exports HSL. If you've ever picked a "50% saturated" color in a design tool and gotten a completely different result in CSS, this mismatch is why.

HSL stands for Hue, Saturation, Lightness. HSV stands for Hue, Saturation, Value (also called HSB — Brightness). They share the same hue wheel but disagree on everything else. Understanding when to use which model saves you from the eternal "why does my CSS color look washed out compared to the mockup" debugging session.

基础原理

The Figma-to-CSS pipeline breaks here

A designer picks a vibrant brand blue in Figma's HSB picker: H=220, S=85%, B=90%. That looks punchy on screen. The developer reads the Figma export, sees HSL values, and writes `hsl(220, 85%, 90%)` — which renders as a pale sky blue, nothing like the mockup. The disconnect: 85% saturation means different things in each model. In HSV/HSB, S=85% means "mostly pure color with little white mixed in." In HSL, S=85% at L=90% means "very light tint with a hint of blue." The fix is converting properly, not eyeballing.

How the models actually differ

| Property | HSL | HSV (HSB) | |---|---|---| | Third axis | Lightness (0% = black, 100% = white, 50% = pure color) | Value/Brightness (0% = black, 100% = fully bright) | | Saturation at max | Full color at S=100%, L=50% | Full color at S=100%, V=100% | | White | S=0%, L=100% | S=0%, V=100% | | Black | Any S, L=0% | Any S, V=0% | | Pure red | hsl(0, 100%, 50%) | hsv(0, 100%, 100%) | | Shape metaphor | Double cone (bicone) | Single cone or cylinder |

Adobe tools default to HSB for a reason

Photoshop, Illustrator, and After Effects all use HSB in their native color pickers. The reasoning: artists think in terms of "how bright is this color" and "how much white have I mixed in." Value (brightness) maps more intuitively to paint mixing. You start with a vivid hue and either darken it (reduce V) or desaturate it (reduce S). HSL's lightness axis, where 50% is the "purest" point and both 0% and 100% kill the color, feels less natural for visual picking.

CSS chose HSL because it's symmetrical

The CSS working group adopted HSL because its lightness axis is symmetrical: 0% always equals black, 100% always equals white, regardless of hue. That makes programmatic manipulation predictable. `lighten(color, 10%)` always moves toward white. `darken(color, 10%)` always moves toward black. In HSV, achieving the same symmetry requires juggling both S and V simultaneously.

When Google's Material Design team converts

Material Design defines tonal palettes using lightness steps (0, 10, 20... 100). Internally they work in HCT (their custom model), but when exporting to web tokens they use HSL. When exporting to Android native, the picker shows HSV. The same palette, two different representations. Teams that don't understand the conversion get mismatched implementations between platforms.

原理详解

The Figma-to-CSS pipeline breaks here

A designer picks a vibrant brand blue in Figma's HSB picker: H=220, S=85%, B=90%. That looks punchy on screen. The developer reads the Figma export, sees HSL values, and writes `hsl(220, 85%, 90%)` — which renders as a pale sky blue, nothing like the mockup. The disconnect: 85% saturation means different things in each model. In HSV/HSB, S=85% means "mostly pure color with little white mixed in." In HSL, S=85% at L=90% means "very light tint with a hint of blue." The fix is converting properly, not eyeballing.

How the models actually differ

| Property | HSL | HSV (HSB) | |---|---|---| | Third axis | Lightness (0% = black, 100% = white, 50% = pure color) | Value/Brightness (0% = black, 100% = fully bright) | | Saturation at max | Full color at S=100%, L=50% | Full color at S=100%, V=100% | | White | S=0%, L=100% | S=0%, V=100% | | Black | Any S, L=0% | Any S, V=0% | | Pure red | hsl(0, 100%, 50%) | hsv(0, 100%, 100%) | | Shape metaphor | Double cone (bicone) | Single cone or cylinder |

Adobe tools default to HSB for a reason

Photoshop, Illustrator, and After Effects all use HSB in their native color pickers. The reasoning: artists think in terms of "how bright is this color" and "how much white have I mixed in." Value (brightness) maps more intuitively to paint mixing. You start with a vivid hue and either darken it (reduce V) or desaturate it (reduce S). HSL's lightness axis, where 50% is the "purest" point and both 0% and 100% kill the color, feels less natural for visual picking.

CSS chose HSL because it's symmetrical

The CSS working group adopted HSL because its lightness axis is symmetrical: 0% always equals black, 100% always equals white, regardless of hue. That makes programmatic manipulation predictable. `lighten(color, 10%)` always moves toward white. `darken(color, 10%)` always moves toward black. In HSV, achieving the same symmetry requires juggling both S and V simultaneously.

When Google's Material Design team converts

Material Design defines tonal palettes using lightness steps (0, 10, 20... 100). Internally they work in HCT (their custom model), but when exporting to web tokens they use HSL. When exporting to Android native, the picker shows HSV. The same palette, two different representations. Teams that don't understand the conversion get mismatched implementations between platforms.

格式对比

The Figma-to-CSS pipeline breaks here

A designer picks a vibrant brand blue in Figma's HSB picker: H=220, S=85%, B=90%. That looks punchy on screen. The developer reads the Figma export, sees HSL values, and writes `hsl(220, 85%, 90%)` — which renders as a pale sky blue, nothing like the mockup. The disconnect: 85% saturation means different things in each model. In HSV/HSB, S=85% means "mostly pure color with little white mixed in." In HSL, S=85% at L=90% means "very light tint with a hint of blue." The fix is converting properly, not eyeballing.

How the models actually differ

| Property | HSL | HSV (HSB) | |---|---|---| | Third axis | Lightness (0% = black, 100% = white, 50% = pure color) | Value/Brightness (0% = black, 100% = fully bright) | | Saturation at max | Full color at S=100%, L=50% | Full color at S=100%, V=100% | | White | S=0%, L=100% | S=0%, V=100% | | Black | Any S, L=0% | Any S, V=0% | | Pure red | hsl(0, 100%, 50%) | hsv(0, 100%, 100%) | | Shape metaphor | Double cone (bicone) | Single cone or cylinder |

Adobe tools default to HSB for a reason

Photoshop, Illustrator, and After Effects all use HSB in their native color pickers. The reasoning: artists think in terms of "how bright is this color" and "how much white have I mixed in." Value (brightness) maps more intuitively to paint mixing. You start with a vivid hue and either darken it (reduce V) or desaturate it (reduce S). HSL's lightness axis, where 50% is the "purest" point and both 0% and 100% kill the color, feels less natural for visual picking.

CSS chose HSL because it's symmetrical

The CSS working group adopted HSL because its lightness axis is symmetrical: 0% always equals black, 100% always equals white, regardless of hue. That makes programmatic manipulation predictable. `lighten(color, 10%)` always moves toward white. `darken(color, 10%)` always moves toward black. In HSV, achieving the same symmetry requires juggling both S and V simultaneously.

When Google's Material Design team converts

Material Design defines tonal palettes using lightness steps (0, 10, 20... 100). Internally they work in HCT (their custom model), but when exporting to web tokens they use HSL. When exporting to Android native, the picker shows HSV. The same palette, two different representations. Teams that don't understand the conversion get mismatched implementations between platforms.

真实案例

The Figma-to-CSS pipeline breaks here

A designer picks a vibrant brand blue in Figma's HSB picker: H=220, S=85%, B=90%. That looks punchy on screen. The developer reads the Figma export, sees HSL values, and writes `hsl(220, 85%, 90%)` — which renders as a pale sky blue, nothing like the mockup. The disconnect: 85% saturation means different things in each model. In HSV/HSB, S=85% means "mostly pure color with little white mixed in." In HSL, S=85% at L=90% means "very light tint with a hint of blue." The fix is converting properly, not eyeballing.

How the models actually differ

| Property | HSL | HSV (HSB) | |---|---|---| | Third axis | Lightness (0% = black, 100% = white, 50% = pure color) | Value/Brightness (0% = black, 100% = fully bright) | | Saturation at max | Full color at S=100%, L=50% | Full color at S=100%, V=100% | | White | S=0%, L=100% | S=0%, V=100% | | Black | Any S, L=0% | Any S, V=0% | | Pure red | hsl(0, 100%, 50%) | hsv(0, 100%, 100%) | | Shape metaphor | Double cone (bicone) | Single cone or cylinder |

Adobe tools default to HSB for a reason

Photoshop, Illustrator, and After Effects all use HSB in their native color pickers. The reasoning: artists think in terms of "how bright is this color" and "how much white have I mixed in." Value (brightness) maps more intuitively to paint mixing. You start with a vivid hue and either darken it (reduce V) or desaturate it (reduce S). HSL's lightness axis, where 50% is the "purest" point and both 0% and 100% kill the color, feels less natural for visual picking.

CSS chose HSL because it's symmetrical

The CSS working group adopted HSL because its lightness axis is symmetrical: 0% always equals black, 100% always equals white, regardless of hue. That makes programmatic manipulation predictable. `lighten(color, 10%)` always moves toward white. `darken(color, 10%)` always moves toward black. In HSV, achieving the same symmetry requires juggling both S and V simultaneously.

When Google's Material Design team converts

Material Design defines tonal palettes using lightness steps (0, 10, 20... 100). Internally they work in HCT (their custom model), but when exporting to web tokens they use HSL. When exporting to Android native, the picker shows HSV. The same palette, two different representations. Teams that don't understand the conversion get mismatched implementations between platforms.

HSV ↔ HSL conversion in TypeScript

/** Convert HSV (all 0-1) to HSL (all 0-1) */
function hsvToHsl(h: number, s: number, v: number): [number, number, number] {
  const l = v * (1 - s / 2);
  const sl = (l === 0 || l === 1) ? 0 : (v - l) / Math.min(l, 1 - l);
  return [h, sl, l];
}

/** Convert HSL (all 0-1) to HSV (all 0-1) */
function hslToHsv(h: number, s: number, l: number): [number, number, number] {
  const v = l + s * Math.min(l, 1 - l);
  const sv = v === 0 ? 0 : 2 * (1 - l / v);
  return [h, sv, v];
}

// Example: Figma shows HSB(220, 85%, 90%)
// Convert to HSL for CSS:
const [h, sl, l] = hsvToHsl(220/360, 0.85, 0.90);
console.log(`hsl(${Math.round(h*360)}, ${Math.round(sl*100)}%, ${Math.round(l*100)}%)`);
// Output: hsl(220, 79%, 52%) — the actual CSS equivalent

// Verify round-trip:
const [h2, sv2, v2] = hslToHsv(h, sl, l);
console.log(`hsv(${Math.round(h2*360)}, ${Math.round(sv2*100)}%, ${Math.round(v2*100)}%)`);
// Output: hsv(220, 85%, 90%) — back to original

复制粘贴到项目即可使用。

💡 高手技巧

工具推荐

The Figma-to-CSS pipeline breaks here

A designer picks a vibrant brand blue in Figma's HSB picker: H=220, S=85%, B=90%. That looks punchy on screen. The developer reads the Figma export, sees HSL values, and writes `hsl(220, 85%, 90%)` — which renders as a pale sky blue, nothing like the mockup. The disconnect: 85% saturation means different things in each model. In HSV/HSB, S=85% means "mostly pure color with little white mixed in." In HSL, S=85% at L=90% means "very light tint with a hint of blue." The fix is converting properly, not eyeballing.

How the models actually differ

| Property | HSL | HSV (HSB) | |---|---|---| | Third axis | Lightness (0% = black, 100% = white, 50% = pure color) | Value/Brightness (0% = black, 100% = fully bright) | | Saturation at max | Full color at S=100%, L=50% | Full color at S=100%, V=100% | | White | S=0%, L=100% | S=0%, V=100% | | Black | Any S, L=0% | Any S, V=0% | | Pure red | hsl(0, 100%, 50%) | hsv(0, 100%, 100%) | | Shape metaphor | Double cone (bicone) | Single cone or cylinder |

Adobe tools default to HSB for a reason

Photoshop, Illustrator, and After Effects all use HSB in their native color pickers. The reasoning: artists think in terms of "how bright is this color" and "how much white have I mixed in." Value (brightness) maps more intuitively to paint mixing. You start with a vivid hue and either darken it (reduce V) or desaturate it (reduce S). HSL's lightness axis, where 50% is the "purest" point and both 0% and 100% kill the color, feels less natural for visual picking.

CSS chose HSL because it's symmetrical

The CSS working group adopted HSL because its lightness axis is symmetrical: 0% always equals black, 100% always equals white, regardless of hue. That makes programmatic manipulation predictable. `lighten(color, 10%)` always moves toward white. `darken(color, 10%)` always moves toward black. In HSV, achieving the same symmetry requires juggling both S and V simultaneously.

When Google's Material Design team converts

Material Design defines tonal palettes using lightness steps (0, 10, 20... 100). Internally they work in HCT (their custom model), but when exporting to web tokens they use HSL. When exporting to Android native, the picker shows HSV. The same palette, two different representations. Teams that don't understand the conversion get mismatched implementations between platforms.

免费工具推荐

用这些免费工具实操你学到的知识: