CSS @property — 커스텀 속성에 타입을 부여하는 방법

11 min read
CSS@property커스텀 속성CSS 변수애니메이션
CSS @property — 커스텀 속성에 타입을 부여하는 방법

CSS 커스텀 속성(--my-color: red)은 편리하지만 아무 값이나 받는다. 숫자를 넣어야 할 곳에 문자열이 들어와도, 색상이 필요한 곳에 잘못된 값이 들어와도 그냥 통과된다. 브라우저 입장에서는 커스텀 속성이 문자열 치환이기 때문이다. @property는 이 느슨한 구조에 타입 계약을 추가한다.


@property란

@property는 CSS 커스텀 속성에 타입 정의를 부여하는 at-rule이다. 일반 커스텀 속성(var())이 "아무거나 받는 문자열 변수"라면, @property로 등록한 속성은 "이 타입만 받는 강타입 변수"가 된다. 비유하면, 일반 박스에 라벨이 붙은 것과 같다. 라벨이 없으면 무엇이 들어있는지 열어봐야 알지만, 라벨이 있으면 잘못된 물건이 들어오는 순간 걸러낼 수 있다.

핵심 포인트: @property는 등록(Registration)이다. 이후 var()로 참조하는 방식은 동일하지만, 브라우저가 그 값을 의미 있는 타입으로 해석한다.

[💡 잠깐! 이 용어는?] at-rule: @로 시작하는 CSS 명령어. @media, @keyframes, @import처럼 일반 선언과 다른 특수 기능을 수행한다.

Firefox 128(2024년 7월)이 지원을 추가하면서 크로스 브라우저 지원이 완성됐다. 그 이전까지는 Chromium 계열에서만 동작했다.


3가지 필수 디스크립터

@property 블록에는 세 가지 디스크립터를 반드시 명시해야 한다.

src/styles/tokens.css
@property --color-blue {
  syntax: "<color>";
  inherits: true;
  initial-value: blue;
}
 
.element {
  color: var(--color-blue);
}
디스크립터역할값 예시
syntax허용되는 타입 정의"<color>", "<length>", "*"
inherits부모에서 자식으로 값 상속 여부true / false
initial-value유효하지 않은 값이 들어왔을 때의 폴백blue, 0px, 1

세 디스크립터 모두 없으면 @property 선언 자체가 무시된다.

[💡 잠깐! 이 용어는?] 디스크립터(Descriptor): at-rule 블록 내부에서 속성을 기술하는 선언. syntax, inherits, initial-value@property 블록의 디스크립터다. 일반 CSS 선언과 구분하기 위해 이렇게 부른다.


지원 타입과 고급 문법

기본 타입

syntax에 쓸 수 있는 기본 타입들이다.

타입 키워드허용하는 값
"<color>"색상 값 (red, #fff, oklch(...) 등)
"<length>"길이 값 (px, rem, vh 등)
"<integer>"정수
"<number>"실수
"<percentage>"백분율
"<length-percentage>"길이 또는 백분율
"*"모든 값 (타입 미지정, 기본 커스텀 속성과 동일)

리스트와 혼합 타입

단일 값 외에 여러 값을 받거나 타입을 조합할 수도 있다.

src/styles/tokens.css
/* 공백으로 구분된 길이 리스트 (예: padding 축약) */
@property --spacing-list {
  syntax: "<length>+";
  inherits: false;
  initial-value: 0px;
}
 
/* 쉼표로 구분된 색상 리스트 */
@property --color-stops {
  syntax: "<color>#";
  inherits: false;
  initial-value: black;
}
 
/* 혼합 타입 — 길이 또는 키워드 */
@property --offset {
  syntax: "<length> | auto";
  inherits: false;
  initial-value: 0px;
}
기호의미예시
+1개 이상, 공백 구분"<length>+"
#1개 이상, 쉼표 구분"<color>#"
|혼합 타입 (OR)"<length> | auto"

initial-value가 핵심인 이유

initial-value는 단순한 기본값이 아니다. 잘못된 값이 들어왔을 때 렌더링을 보호하는 방어선이다.

일반 커스텀 속성이라면 잘못된 값이 들어왔을 때 속성 자체가 무효화되어 레이아웃이 깨질 수 있다. @property로 등록된 속성은 유효하지 않은 값이 설정되면 initial-value로 조용히 폴백한다. 사용자에게는 아무 문제 없이 렌더링된다.

src/styles/tokens.css
@property --button-radius {
  syntax: "<length>";
  inherits: false;
  initial-value: 4px;
}
 
/* 잘못된 값이 들어와도 4px로 폴백된다 */
.button {
  border-radius: var(--button-radius);
  --button-radius: notALength; /* 무효, 4px로 대체됨 */
}

initial-value의 제약사항

주의할 것이 있다. initial-value에는 모든 값을 쓸 수 있는 게 아니다.

src/styles/tokens.css
/* ❌ 불가 — em/rem 같은 상대 단위 */
@property --size {
  syntax: "<length>";
  inherits: true;
  initial-value: 1rem; /* 무효 */
}
 
/* ❌ 불가 — CSS 함수 */
@property --fluid-size {
  syntax: "<length>";
  inherits: true;
  initial-value: clamp(1rem, 5vw, 2rem); /* 무효 */
}
 
/* ❌ 불가 — 다른 커스텀 속성 참조 */
@property --derived {
  syntax: "<length>";
  inherits: true;
  initial-value: var(--base-size); /* 무효 */
}
 
/* ✅ 예외 — currentColor는 허용 */
@property --icon-color {
  syntax: "<color>";
  inherits: true;
  initial-value: currentColor;
}

initial-value절대값(absolute value) 만 허용한다. 상대 단위, CSS 함수, 다른 커스텀 속성 참조는 모두 불가다. currentColor만 예외적으로 허용된다.

[💡 잠깐! 이 용어는?] 절대값(Absolute Value): 다른 요소나 컨텍스트에 의존하지 않고 그 자체로 완결된 값. 16px, #ffffff, 1처럼 계산 없이 확정된 값이다. 반대로 1rem, 50%, var(--x)는 참조 대상이 있어야 계산되는 상대값이다.


동적 값 우회 패턴

initial-valueclamp()rem을 쓸 수 없어서 유동적인 기본값을 설정하려면, 등록과 할당을 분리하면 된다.

src/styles/tokens.css
@property --heading-size {
  syntax: "<length>";
  inherits: true;
  initial-value: 24px; /* 절대값만 허용 */
}
 
/* 루트에서 실제 사용할 값을 오버라이드 */
:root {
  --heading-size: clamp(1.25rem, 5cqi, 2rem);
}

@propertyinitial-value는 진짜 폴백(아무 값도 없을 때 사용)이고, :root에서 설정하는 값이 실제로 쓰이는 동적 기본값이다. 구조를 나누는 것이 포인트다.


타입이 있으면 애니메이션이 된다

@property의 가장 강력한 부수효과는 애니메이션 가능성이다. 일반 커스텀 속성은 브라우저가 타입을 모르기 때문에 보간(interpolation)을 할 수 없다. 타입이 정의되면 브라우저가 두 값 사이를 매끄럽게 보간할 수 있다.

src/styles/animation.css
@property --hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
 
.button {
  background: hsl(var(--hue) 80% 50%);
  transition: --hue 0.4s ease;
}
 
.button:hover {
  --hue: 240;
}

--hue에 타입이 없다면 transition이 동작하지 않는다. 값이 문자열이라 중간값을 계산할 수 없기 때문이다. <number> 타입을 부여하면 0에서 240까지 숫자가 보간되고, 배경색이 부드럽게 변한다.

비유하면, 타입 없는 커스텀 속성 애니메이션은 볼륨 노브가 없는 스피커다. 소리가 갑자기 켜지거나 꺼질 뿐, 부드럽게 조절할 방법이 없다. @property로 타입을 정의하면 노브가 생긴다.


JavaScript API와 우선순위

@property와 동일한 기능을 JavaScript에서도 사용할 수 있다.

src/scripts/register-properties.js
CSS.registerProperty({
  name: '--color-blue',
  syntax: '<color>',
  inherits: true,
  initialValue: 'blue',
})

CSS와 JavaScript 중 어떤 방식을 선택해도 동작은 동일하다. 다만 동일한 프로퍼티를 중복 등록하면 마지막 정의가 우선한다. CSS @propertyCSS.registerProperty()가 같은 프로퍼티를 등록하면 나중에 파싱된 쪽이 이긴다.


DevTools 활용

Chrome DevTools의 Styles 패널에서 @property로 등록된 속성을 확인할 수 있다. 유효하지 않은 값이 설정되면 노란색 경고 아이콘과 함께 오류 메시지가 표시된다. 어떤 값이 무효 처리되고 폴백이 적용됐는지 즉시 파악할 수 있어 디버깅이 한층 수월해진다.


프로그레시브 인핸스먼트로 사용하기

@property를 지원하지 않는 환경을 위한 방어 코드를 작성하는 것이 권장된다.

src/styles/tokens.css
/* 기본 폴백 — @property 미지원 환경 */
:root {
  --accent-color: blue;
}
 
/* @property 지원 환경에서 타입 정의 덮어쓰기 */
@supports (syntax: "<color>") {
  @property --accent-color {
    syntax: "<color>";
    inherits: true;
    initial-value: blue;
  }
}

@supports@property 지원 여부를 확인하고, 지원하는 경우에만 타입 정의를 적용한다. 지원하지 않는 환경에서도 커스텀 속성 자체는 동작한다.


마무리

@property는 CSS 커스텀 속성을 "타입 있는 변수"로 격상시킨다. Firefox 128 이후 크로스 브라우저 지원이 완성됐고, 이제 프로덕션에서 사용해도 무방한 단계다.

  • syntax로 허용 타입을 명시하면 브라우저가 값을 의미 있게 해석한다
  • initial-value는 폴백을 보장해 잘못된 값이 들어와도 렌더링이 깨지지 않는다
  • 타입이 정의되면 transitionanimation을 통한 값 보간이 가능해진다
  • initial-value에는 절대값만 허용된다 — 동적 기본값은 :root에서 오버라이드로 설정한다
  • @supports로 감싸 프로그레시브 인핸스먼트 방식으로 사용하는 것을 권장한다

참고:

관심 있을 만한 포스트

CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스

CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.

CSS:near()

CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간

Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.

CSSbase-select

sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션

CSS sibling-index()와 scroll-driven animations를 결합해 순수 CSS만으로 텍스트 보텍스 효과를 구현하는 기법을 다룬다.

CSSsibling-index

Interop 2026 — 브라우저 전쟁이 끝나고 표준 전쟁이 시작됐다

Chrome, Safari, Firefox가 합의한 20개 웹 표준 집중 영역과 프론트엔드 개발자가 주목해야 할 핵심 기능을 정리한다.

InteropCSS

CSS Stacking Context — z-index: 99999를 줬는데 왜 안 올라올까

CSS 쌓임 맥락의 생성 조건, z-index의 실제 작동 원리, 그리고 레이아웃 버그를 디버깅하는 실전 전략을 정리한다.

CSSz-index

OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기

conic-gradient, CSS 커스텀 프로퍼티, 새로운 attr() 타입 구문을 활용해 JavaScript 없이 시맨틱한 파이 차트를 구현하는 방법을 다룬다.

CSSconic-gradient

배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법

브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.

CSScontrast-color

포크레인 없이 못 박기 — CSS 수학 함수만으로 바 차트 그리기

CSS 수학 함수와 커스텀 프로퍼티, 시맨틱 HTML을 조합해 JavaScript 없이 반응형 바 차트를 구현하는 방법을 다룬다.

CSScalc()