CSS 명시도 계산기로 우선순위 확인하기

CSS 선택자 두 개가 같은 요소를 가리킬 때 어느 쪽이 실제로 적용될지는 명시도(specificity)라는 A-B-C 세 자리 점수로 정해집니다. 이 글은 그 점수가 어떤 규칙으로 계산되는지, 왜 분명히 나중에 쓴 스타일이 적용되지 않는지 계산 원리만 정리합니다. 자신이 쓴 선택자의 점수를 직접 확인하고 싶은 프런트엔드 개발자와 퍼블리셔를 위한 글입니다.
요약 ① 명시도는 A(ID)·B(클래스·속성·가상 클래스)·C(태그·가상 요소) 세 자리 숫자이며, 비교는 A→B→C 순서로 자릿수를 따집니다. ② :not()·:is()·:has() 괄호 안에서는 가장 명시도가 높은 선택자 하나만 반영되고, :where()는 항상 0으로 계산됩니다. ③ !important는 명시도 항목이 아니라 별도의 우선순위 계층이며, 명시도가 같으면 나중에 선언된 규칙이 이깁니다.
왜 이 문제가 생기나
실무에서 자주 나오는 상황으로 설명합니다. 스타일시트 위쪽에 #hero .cta a { color: crimson; }가 있고, 그 아래에 .button-primary { color: royalblue; }를 새로 추가했다고 가정합니다.
코드 작성 순서로는 .button-primary가 나중에 나오니 이 색이 적용될 것 같지만, 화면에는 여전히 crimson이 남습니다.
브라우저는 "나중에 쓴 규칙"이 아니라 "명시도가 더 높은 규칙"을 먼저 따르기 때문입니다. #hero .cta a는 ID 선택자가 포함돼 A값이 1이고, .button-primary는 클래스 선택자 하나뿐이라 A값이 0입니다. A값이 0인 규칙은 A값이 1인 규칙을 코드 순서와 무관하게 절대 넘어설 수 없습니다.
손으로 계산하는 법
명시도는 도구 없이도 손으로 계산할 수 있습니다. 선택자를 구성 요소별로 나눠 세 자리 숫자 A-B-C로 정리하면 됩니다.
A-B-C 자리 채우기
A — ID 선택자. #id 형태로 쓰인 선택자 개수를 셉니다.
B — 클래스·속성·가상 클래스. .class, [attr], :hover·:focus·:nth-child() 같은 가상 클래스 개수를 모두 더합니다.
C — 태그·가상 요소. div, a, p 같은 태그 이름과 ::before, ::after 같은 가상 요소 개수를 더합니다. 콤비네이터(스페이스, >, +, ~)와 전체 선택자(*)는 어느 자리에도 들어가지 않습니다.
예시로 확인하기
예를 들어 #main .nav > a:hover를 세어 보면 이렇습니다.
#main→ ID 선택자 1개 → A = 1.nav→ 클래스 선택자 1개 → B = 1>→ 콤비네이터, 계산에서 제외a→ 태그 선택자 1개 → C = 1:hover→ 가상 클래스 1개 → B = 1 (누적 B = 2)
합산하면 A=1, B=2, C=1이므로 명시도는 1, 2, 1입니다.
우선순위를 비교하는 법
두 선택자의 우선순위를 비교할 때는 이 세 자리를 각각 따로 비교합니다. 먼저 A값이 큰 쪽이 무조건 이기고, A값이 같으면 B값을, B값도 같으면 C값을 비교합니다.
B값이 아무리 커도 10진수처럼 A로 올림되지 않습니다. 즉 B=13인 선택자는 A=1인 선택자를 절대 이기지 못합니다.
함수형 가상 클래스는 다르게 센다
:not(), :is(), :has()처럼 괄호 안에 다른 선택자를 넣는 함수형 가상 클래스는 계산 방식이 다릅니다. 괄호 안에 콤마로 나열된 선택자 중 명시도가 가장 높은 것 하나만 전체 값에 더해집니다.
예를 들어 a:not(#nav, .btn)이라면 괄호 안의 #nav(A=1)와 .btn(B=1) 중 더 높은 #nav만 반영되어, 태그 a(C=1)와 합쳐 최종 명시도는 1, 0, 1이 됩니다.
:where()는 예외입니다. 괄호 안에 무엇이 들어 있든 명시도 기여가 항상 0입니다. 같은 선택자를 a:where(#nav, .btn)으로 바꾸면 괄호 안 내용과 무관하게 태그 a의 C=1만 남아 최종 명시도는 0, 0, 1이 됩니다.
이 계산을 선택자마다 손으로 반복하는 건 번거롭고, 선택자가 길어지거나 :not()이 중첩되면 실수하기도 쉽습니다. 저희 도구로 확인하기.
선택자를 입력하면 A(ID)·B(클래스)·C(태그) 값과 최종 명시도가 즉시 계산됩니다. 입력한 내용은 브라우저 밖으로 전송되지 않습니다.

자주 헷갈리는 지점
초보자가 자주 헷갈리는 지점 세 가지가 있습니다.
!important는 명시도 항목이 아닙니다. A-B-C 어디에도 들어가지 않는, 완전히 별도의 우선순위 계층입니다. !important가 붙은 선언은 명시도와 무관하게 붙지 않은 선언보다 먼저 적용됩니다.
소스 코드에 적은 순서와 명시도는 다른 개념입니다. "나중에 쓴 코드가 이긴다"는 규칙은 명시도가 같을 때만 적용됩니다. 명시도가 다르면 코드 순서와 무관하게 명시도가 높은 쪽이 이깁니다.
명시도가 같으면 나중에 선언된 규칙이 이깁니다. 예를 들어 .a.b와 .c.d는 둘 다 A=0, B=2, C=0으로 명시도가 같습니다. 이 경우에는 스타일시트에서 더 나중에 쓰인 규칙이 적용됩니다.
| 구분 | 명시도(A-B-C)에 포함되나 | 우선순위가 갈리는 시점 |
|---|---|---|
!important | 아니요, 별도의 우선순위 계층 | 명시도 비교 이전에 먼저 적용 |
| 소스 코드 순서 | 아니요 | 명시도가 완전히 같을 때만 결정 |
인라인 스타일(style="") | 선택자가 없어 A-B-C 계산 대상이 아님 | 일반 선택자 규칙보다 먼저 적용되는 별도 계층 |
이 계산기는 입력한 선택자 문자열만 분석하므로, 인라인 스타일 자체의 우선순위는 계산 대상에 포함되지 않습니다.
정리
- 명시도는 A(ID)-B(클래스·속성·가상 클래스)-C(태그·가상 요소) 세 자리 숫자이고, 비교는 A→B→C 순서로만 이뤄집니다.
- B나 C값이 아무리 커도 상위 자릿수(A)를 이길 수 없습니다.
:not()·:is()·:has()는 괄호 안에서 가장 명시도가 높은 선택자 하나만 반영되고,:where()는 항상 0입니다.!important는 명시도가 아니라 별도의 우선순위 계층이며, 소스 코드 순서는 명시도가 같을 때만 승부를 가릅니다.- 손으로 세기 헷갈리면 선택자를 그대로 입력해 A-B-C 값을 바로 확인할 수 있습니다.
선택자를 입력하면 A(ID)·B(클래스)·C(태그) 값과 최종 명시도가 즉시 계산됩니다. 입력한 내용은 브라우저 밖으로 전송되지 않습니다.
자주 묻는 질문
명시도가 같으면 어떤 규칙이 적용되나요?
소스 코드에서 더 나중에(또는 더 아래에) 선언된 규칙이 적용됩니다. 다만 이 규칙은 두 선택자의 A-B-C가 완전히 같을 때만 적용되며, 하나라도 다르면 명시도가 높은 쪽이 코드 순서와 무관하게 이깁니다.
!important를 쓰면 항상 최우선으로 적용되나요?
!important가 붙은 선언은 붙지 않은 일반 선언보다 항상 먼저 적용됩니다. 다만 !important끼리 같은 속성을 두고 충돌하면, 그 안에서 다시 명시도와 코드 순서로 우선순위가 갈립니다.
:where()는 왜 명시도에 전혀 기여하지 않나요?
CSS 명세가 :where()를 그렇게 정의하고 있기 때문입니다. :is()나 :not()은 괄호 안에서 가장 명시도가 높은 선택자를 반영하지만, :where()는 괄호 안에 무엇을 넣든 항상 0으로 계산됩니다. 그래서 명시도에 영향을 주지 않으면서 선택자를 그룹으로 묶을 때 활용됩니다.
콜론을 하나만 쓴 :before도 가상 요소로 인식되나요?
네. ::before처럼 콜론 두 개를 쓰는 것이 표준 표기법이지만, :before·:after·:first-line·:first-letter처럼 콜론 하나만 쓰는 과거 CSS2 방식도 가상 요소로 인식되어 C값에 더해집니다. 반면 :hover·:focus처럼 원래부터 콜론 하나만 쓰는 가상 클래스는 B값에 더해집니다.
