CSS Color Functions

작성일:2026.01.03|수정일:2026.07.07|조회수:6

CSS Color Functions

이 포스트는 Sunkanmi Fafowora가 CSS-Tricks에 올린 CSS Color Functions 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는 사견이 포함되어 있기도 하다.

웹에서 색을 다루는 일은 이상하게 만만해 보인다. #000, red, rgb() 같은 값은 너무 오래 봐서 거의 배경음악처럼 느껴지고, 실제로 간단한 UI에서는 그 정도로도 충분한 경우가 많다. 그런데 조금만 더 들어가면 이야기가 달라진다. 같은 파란색을 조금 더 밝게 만들고 싶다거나, 브랜드 색에서 hover 색을 뽑고 싶다거나, 그라디언트가 중간에서 죽은 회색처럼 보이지 않게 만들고 싶을 때, 색은 갑자기 꽤 까다로운 문제가 된다.

CSS의 색상 함수는 그 까다로움을 줄이기 위한 도구에 가깝다. 단순히 색을 적는 문법이 늘어난 것이 아니라, 어떤 색 공간에서 색을 보고, 어떤 방식으로 섞고, 어떤 기준으로 변형할 것인지 선택할 수 있게 된 것이다.

색 공간을 먼저 생각해야 한다

색 공간(color space)은 색을 좌표로 정리하는 방식이다. 같은 색처럼 보여도 어떤 색 공간에서 표현하느냐에 따라 값의 의미가 달라진다. CSS에서 가장 익숙한 색 공간은 sRGB다. 오래전부터 웹의 기본값처럼 쓰였고, hex, rgb(), hsl(), hwb() 같은 표기는 모두 sRGB 쪽에 속한다.

sRGB가 나쁜 것은 아니다. 호환성이 좋고, 오래된 도구와 디자인 시스템에도 잘 맞는다. 문제는 sRGB가 현대 디스플레이가 표현할 수 있는 색을 모두 담지는 못한다는 점이다. display-p3, lab, lch, oklab, oklch 같은 색 공간을 쓰면 더 넓은 색 영역을 다루거나, 사람이 느끼기에 더 자연스러운 방식으로 색을 조정할 수 있다.

내가 보기에는 여기서 중요한 질문은 “최신 색상 함수를 써야 하는가?”가 아니라 “이 색을 어떤 기준으로 다룰 것인가?”다. 버튼 배경색을 조금 밝게 만들고 싶은 것인지, 실제 빛의 혼합에 가까운 결과가 필요한 것인지, 아니면 눈으로 보기에 일정한 명도 단계를 만들고 싶은 것인지에 따라 선택이 달라진다.

sRGB: 여전히 기본값인 색 공간

sRGB 기반 색상은 지금도 가장 많이 쓰인다. 오래된 코드와 디자인 도구, 브라우저 호환성을 생각하면 여전히 안전한 기본값이다.

rgb()

rgb()는 빨강, 초록, 파랑 채널로 색을 표현한다. 최신 문법에서는 쉼표 대신 공백으로 값을 구분하고, 알파 값은 슬래시 뒤에 쓴다.

CSS
.card {
  color: rgb(245 123 151);
  background-color: rgb(245 123 151 / 20%);
}

기존 쉼표 문법도 동작한다.

CSS
.card {
  color: rgb(245, 123, 151);
  background-color: rgb(245, 123, 151, 0.2);
}

다만 한 색상 값 안에서 두 문법을 섞으면 안 된다.

CSS
/* 유효하지 않음 */
.card {
  color: rgb(245, 123, 151 / 0.2);
}

rgba()는 이제 특별한 함수라기보다 rgb()의 별칭에 가깝다. rgb()에서도 알파 값을 쓸 수 있으므로 새로 작성하는 코드에서는 rgb()만 써도 충분하다.

CSS
.card-a {
  color: rgba(250 30 45 / 0.8);
}

.card-b {
  color: rgb(250 30 45 / 0.8);
}

둘은 같은 방식으로 동작한다. 그래서 특별한 이유가 없다면 rgb()로 통일하는 편이 덜 헷갈린다.

16진수 색상

16진수 색상은 sRGB 값을 짧게 적는 방식이다. #RRGGBB가 가장 익숙한 형태이고, 알파까지 포함하면 #RRGGBBAA를 쓸 수 있다.

CSS
.badge {
  color: #aabbcc;
  background-color: #aabbcc80;
}

3자리와 4자리 표기는 축약형이다. 각 문자가 한 번씩 반복되어 해석된다.

CSS
.badge {
  color: #abc;  /* #aabbcc */
  background-color: #abc8; /* #aabbcc88 */
}

짧아서 편하지만, 정밀하게 색을 관리해야 하는 디자인 시스템에서는 오히려 불편할 수 있다. 색상 토큰을 만들 때는 사람이 읽고 조정하기 쉬운 형식이 더 중요할 때가 많다.

hsl()hwb()

hsl()은 색상(hue), 채도(saturation), 명도(lightness)로 색을 표현한다. 빨강/초록/파랑 채널보다 사람이 조정하기 쉬워 보인다는 장점이 있다.

CSS
.alert {
  color: hsl(340deg 80% 55%);
  background-color: hsl(340deg 80% 55% / 12%);
}

hsla()rgba()와 비슷하다. hsl()이 이미 알파 값을 지원하므로 새 코드에서는 hsl()만 써도 된다.

CSS
.alert-a {
  color: hsla(340deg 80% 55% / 0.8);
}

.alert-b {
  color: hsl(340deg 80% 55% / 0.8);
}

hwb()는 색상(hue)에 흰색(whiteness)과 검은색(blackness)의 비율을 더해 색을 만든다.

CSS
.note {
  color: hwb(210deg 20% 10%);
  background-color: hwb(210deg 80% 5% / 0.4);
}

실무에서 hwb()를 자주 보지는 못했지만, 특정 색에 흰색이나 검은색을 섞는 식으로 생각하고 싶을 때는 꽤 직관적이다.

이름 있는 색상

red, blue, white, rebeccapurple 같은 이름 있는 색상도 CSS 색상 값이다. 이것들도 sRGB 기반이다.

CSS
.link {
  color: rebeccapurple;
}

다만 이름이 사람이 기대하는 색과 늘 잘 맞지는 않는다. 빠른 예제나 데모에서는 편하지만, 제품 UI의 색상 시스템에서는 명시적인 색상 토큰을 쓰는 편이 낫다.

Lab, LCH, Oklab, OKLCH

sRGB 기반 함수가 기계와 오래된 웹의 관성에 가깝다면, lab(), lch(), oklab(), oklch()는 사람이 색을 느끼는 방식에 조금 더 가까이 가려는 시도다. 특히 명도나 채도를 단계적으로 조정할 때 차이가 난다.

lab()lch()

lab()은 명도(lightness), 초록-빨강 축(a), 파랑-노랑 축(b)으로 색을 표현한다.

CSS
.panel {
  color: lab(55% 40 25);
  background-color: lab(90% 5 10 / 0.5);
}

lch()는 같은 계열의 색을 명도(lightness), 크로마(chroma), 색상(hue)으로 표현한다. lab()보다 사람이 읽기에는 조금 편하다.

CSS
.panel {
  color: lch(55% 60 320deg);
}

다만 CSS에서 색을 새로 설계한다면 lch()보다 oklch()를 먼저 검토하는 편이 좋다고 생각한다. Oklab 계열이 더 일관된 결과를 주는 경우가 많기 때문이다.

oklab()oklch()

Oklab은 기존 Lab의 한계를 줄이기 위해 만들어진 색 공간이다. CSS에서는 oklab()oklch()로 쓴다. 특히 oklch()는 요즘 CSS 색상 시스템에서 가장 실용적인 선택지 중 하나다.

CSS
:root {
  --brand: oklch(62% 0.18 250);
  --brand-light: oklch(72% 0.14 250);
  --brand-dark: oklch(48% 0.20 250);
}

oklch()의 세 값은 명도, 크로마, 색상이다.

이 구조는 색상 토큰을 만들 때 꽤 편하다. 같은 hue를 유지하면서 명도만 바꾸거나, 명도는 유지하고 크로마만 낮추는 식의 조정이 자연스럽다.

CSS
.button {
  --button-bg: oklch(62% 0.18 250);

  color: white;
  background-color: var(--button-bg);
}

.button:hover {
  background-color: oklch(56% 0.18 250);
}

물론 oklch()라고 해서 무조건 안전한 것은 아니다. 어떤 값은 sRGB 화면에서 표현할 수 없는 색이 될 수 있다. 브라우저는 이런 색을 표시 가능한 색으로 매핑하지만, 그 결과가 디자인 의도와 다를 수 있다. 그래서 넓은 색 영역을 쓰는 색상은 실제 디스플레이와 브라우저에서 확인해야 한다.

color() 함수

color()는 특정 색 공간을 직접 지정해 색을 적는 함수다. display-p3 같은 넓은 색 영역을 명시적으로 쓰고 싶을 때 유용하다.

CSS
.hero {
  background-color: color(display-p3 0.1 0.45 0.95);
}

다만 이 값은 모든 환경에서 같은 감각으로 보인다고 기대하기 어렵다. 특히 display-p3 같은 색은 해당 색 영역을 표현할 수 있는 디스플레이에서 의미가 커진다. 넓은 색 영역을 쓸수록 fallback을 어떻게 둘지 같이 생각해야 한다.

CSS
.hero {
  background-color: rgb(20 90 220);
  background-color: color(display-p3 0.08 0.36 0.9);
}

CSS에서는 뒤에 선언한 값이 앞의 값을 덮는다. color(display-p3 ...)를 이해하는 브라우저는 두 번째 값을 쓰고, 그렇지 않은 브라우저는 앞의 sRGB 값을 유지한다. 단순하지만 아직도 가장 읽기 쉬운 fallback 방식이다.

color-mix()로 색 섞기

color-mix()는 두 개 이상의 색을 지정한 색 공간에서 섞는다. hover 색, border 색, 약한 배경색을 만들 때 특히 편하다.

CSS
.card {
  --brand: oklch(62% 0.18 250);

  border-color: color-mix(in oklab, var(--brand), white 70%);
  background-color: color-mix(in oklab, var(--brand), white 92%);
}

여기서 중요한 값은 in oklab이다. 색을 어떤 공간에서 섞을지 정하는 부분이다. 같은 빨강과 파랑을 섞어도 srgb, oklab, oklch에서 결과가 달라진다.

CSS
.example-srgb {
  background: color-mix(in srgb, red, blue);
}

.example-oklab {
  background: color-mix(in oklab, red, blue);
}

.example-oklch {
  background: color-mix(in oklch, red, blue);
}

대략적인 판단 기준은 이렇다.

극좌표 색 공간인 hsl, lch, oklch에서는 hue를 어느 방향으로 돌릴지도 정할 수 있다.

CSS
.gradient-a {
  background: linear-gradient(
    90deg,
    color-mix(in oklch shorter hue, red, blue),
    color-mix(in oklch longer hue, red, blue)
  );
}

이런 코드는 예제에서는 재미있지만, 제품 코드에서는 지나치게 마술처럼 보일 수 있다. 색상 시스템에 넣는다면 왜 그 색 공간을 선택했는지 팀 안에서 설명 가능해야 한다.

상대 색상 문법

상대 색상(relative color) 문법은 기준 색에서 채널을 꺼내 새 색을 만드는 방식이다. 색상 토큰 하나에서 변형 색을 만들 때 꽤 유용하다.

CSS
:root {
  --brand: oklch(62% 0.18 250);
}

.button {
  background-color: var(--brand);
}

.button:hover {
  background-color: oklch(from var(--brand) calc(l - 0.08) c h);
}

from var(--brand)가 기준 색이다. 브라우저는 이 색을 oklch() 기준으로 변환한 뒤 l, c, h, alpha 채널을 사용할 수 있게 만든다. 위 예제는 hue와 chroma는 유지하고 명도만 낮춘다.

알파 값도 같은 방식으로 다룰 수 있다.

CSS
.overlay {
  --brand: oklch(62% 0.18 250);

  background-color: oklch(from var(--brand) l c h / 0.18);
}

주의할 점은 상대 색상에서 꺼낸 채널 값이 계산용 숫자로 다뤄진다는 것이다. 예를 들어 oklch()l은 상대 색상 계산 안에서 0부터 1 사이 숫자로 노출된다. 그래서 calc(l + 10%)처럼 쓰면 기대와 다르게 유효하지 않을 수 있고, 보통은 숫자를 더한다.

CSS
/* 명도를 약간 올린 색 */
.highlight {
  color: oklch(from var(--brand) calc(l + 0.08) c h);
}

color() 함수에서 상대 색상을 쓸 때는 색 공간 이름을 함께 적는다.

CSS
.sample {
  color: color(from var(--brand) srgb r g b / 0.7);
}

반면 color-mix() 자체가 상대 색상 문법이 되는 것은 아니다. 필요하다면 color-mix()에 넘기는 색상 값 안에서 상대 색상을 만들면 된다.

CSS
.card {
  --brand: oklch(62% 0.18 250);

  background-color: color-mix(
    in oklab,
    oklch(from var(--brand) l c h / 0.18),
    white 80%
  );
}

fallback은 아직 필요하다

최신 색상 함수의 브라우저 지원은 꽤 좋아졌지만, 그렇다고 fallback을 전혀 생각하지 않아도 된다는 뜻은 아니다. 특히 메일 클라이언트, 임베디드 웹뷰, 오래된 브라우저, 캡처/렌더링 도구까지 포함하면 사정이 달라진다.

가장 단순한 방식은 sRGB 값을 먼저 쓰고, 그 뒤에 최신 색상 값을 쓰는 것이다.

CSS
.button {
  background-color: #3457d5;
  background-color: oklch(52% 0.20 265);
}

더 분명하게 나누고 싶다면 @supports를 쓸 수 있다.

CSS
.button {
  --button-bg: #3457d5;

  background-color: var(--button-bg);
}

@supports (color: oklch(50% 0.1 250)) {
  .button {
    --button-bg: oklch(52% 0.20 265);
  }
}

color-mix()도 마찬가지다.

CSS
.badge {
  border-color: rgb(52 87 213 / 30%);
  background-color: rgb(52 87 213 / 10%);
}

@supports (color: color-mix(in oklab, blue, white)) {
  .badge {
    --brand: oklch(52% 0.20 265);

    border-color: color-mix(in oklab, var(--brand), white 70%);
    background-color: color-mix(in oklab, var(--brand), white 90%);
  }
}

이렇게 하면 최신 문법을 지원하는 환경에서는 더 의도에 가까운 색을 쓰고, 그렇지 않은 환경에서는 최소한 깨지지 않는 색을 제공할 수 있다.

어떤 색 공간을 쓸 것인가

정리하면 색상 함수 선택은 취향 문제가 아니라 판단 기준의 문제에 가깝다.

색은 숫자로 적히지만, 결국 사람이 보는 값이다. 그래서 좋은 CSS 색상 코드는 최신 문법을 많이 쓰는 코드가 아니라, 어떤 색 공간에서 어떤 의도로 값을 조정하는지 드러나는 코드라고 생각한다.

댓글

댓글을 불러오는 중...