Counter styles/Using counters

김동현·2026년 3월 21일

mdn 학습 번역 - CSS

목록 보기
73/190

안녕하세요! 프론트엔드 개발 강사입니다. CSS의 숨겨진 꿀단지 같은 기능인 CSS 카운터(CSS Counters)를 공부하려고 하시는군요!

보통 리스트 번호를 매길 때 HTML의 <ol> 태그를 떠올리시겠지만, 디자인 요구사항이 복잡해지면 <ol>만으로는 한계에 부딪힐 때가 많아요. 그럴 때 바로 이 CSS 카운터가 구세주가 되어줍니다. 영문 문서라 조금 딱딱하게 느껴지셨을 텐데, 제가 실무 팁을 팍팍 섞어서 이해하기 쉬운 구어체로 싹 번역해 드릴게요. 천천히 따라와 보세요! 😊


CSS 카운터 사용하기 (Using CSS counters)

CSS 카운터를 사용하면 문서 내의 위치에 따라 콘텐츠의 모양을 동적으로 조정할 수 있습니다.
예를 들어, 카운터를 사용하면 웹페이지의 제목(<h1>, <h2> 등)에 자동으로 번호를 매기거나, 순서가 있는 리스트(<ol>)의 번호 매기기 방식을 마음대로 바꿀 수 있답니다.

본질적으로 카운터는 CSS가 유지하고 관리하는 변수라고 생각하시면 돼요. 이 변수의 값은 CSS 규칙을 통해 몇 번 사용되었는지 추적하면서 증가(increment)하거나 감소(decrement)할 수 있습니다. 요소에 있는 카운터 값은 다음과 같은 요소들에 의해 영향을 받습니다:

  1. 카운터는 부모 요소로부터 상속(inherited)되거나 이전 형제 요소로부터 전달됩니다.
  2. 새로운 카운터는 counter-reset 속성을 사용해 인스턴스화(초기화)됩니다.
  3. 카운터 값은 counter-increment 속성을 사용해 증가시킵니다.
  4. 카운터 값은 counter-set 속성을 사용해 특정 값으로 직접 설정할 수도 있습니다.

여러분은 원하는 이름으로 자신만의 카운터를 정의할 수 있고, 모든 순서 있는 리스트(<ol>)에 기본적으로 생성되는 list-item이라는 이름의 카운터를 직접 조작할 수도 있습니다.

💡 강사의 실무 팁! "왜 CSS 카운터를 써야 할까요?"
실무에서 약관 페이지나 목차를 만들 때 "1.1.2", "가.나.다" 처럼 다단계로 깊어지는 넘버링 디자인을 요청받는 경우가 많아요. HTML <ol>만 쓰면 CSS로 예쁘게 꾸미기 까다롭고 커스텀 기호를 넣기 힘들지만, CSS 카운터를 쓰면 ::before 가상 요소를 이용해 내가 원하는 위치에, 원하는 색상과 크기로, 원하는 형태의 번호를 마음껏 찍어낼 수 있습니다!


이 문서의 내용


카운터 사용하기 (Using counters)

카운터를 사용하려면 가장 먼저 counter-reset 속성을 사용해 값을 초기화해야 합니다.
초기화된 카운터의 값은 counter-increment 속성을 사용해 증가시키거나 감소시킬 수 있고, counter-set 속성을 사용해 특정 값으로 강제 지정할 수도 있어요.
현재 카운터의 값을 화면에 보여줄 때는 주로 가상 요소(pseudo-element)content 속성 안에서 counter() 또는 counters() 함수를 사용합니다.

주의할 점은, 카운터는 렌더링 박스를 생성하는 요소에서만 설정, 초기화, 또는 증가될 수 있다는 것입니다.
예를 들어, 어떤 요소에 display: none이 설정되어 있다면 그 요소에 적용된 모든 카운터 조작은 브라우저가 무시해 버립니다. (화면에 안 그리는 요소니까 카운트도 안 세는 거죠!)

또한, 카운터의 속성들은 contain 속성에서 자세히 설명하고 있는 스타일 컨테인먼트(style containment)를 사용해 특정 요소로만 범위를 제한(scoped)할 수도 있습니다.

카운터 값 조작하기 (Manipulating a counter's value)

CSS 카운터를 사용하려면, 반드시 먼저 counter-reset 속성으로 값을 초기화해야 합니다.
이 속성은 카운터 값을 특정한 숫자로 변경할 때도 사용할 수 있습니다.

아래 코드에서는 section이라는 이름의 카운터를 기본값인 0으로 초기화하고 있습니다.

counter-reset: section;

한 번에 여러 개의 카운터를 초기화할 수도 있고, 각각의 초기값을 선택적으로 지정해 줄 수도 있어요.
아래 코드에서는 sectiontopic 카운터를 기본값(0)으로 초기화하고, page 카운터는 3으로 초기화하고 있습니다.

counter-reset: section page 3 topic;

초기화가 끝나면, 카운터 값은 counter-increment를 사용해 늘리거나 줄일 수 있습니다.
예를 들어, 다음 선언은 모든 <h3> 태그를 만날 때마다 section 카운터 값을 1씩 증가시킵니다.

h3::before {
  counter-increment: section; /* section 카운터 값을 1 증가시킵니다 */
}

카운터 이름 뒤에 증가하거나 감소할 양을 직접 지정할 수도 있습니다. 양수나 음수 모두 가능하며, 숫자를 적어주지 않으면 기본값인 1이 적용됩니다.

증가나 감소 외에도, counter-set 속성을 사용하면 카운터를 명시적인 값으로 바로 맞춰버릴 수 있습니다.

.done::before {
  counter-set: section 20;
}

참고로 카운터의 이름은 예약어인 none, inherit, 또는 initial이 되어서는 안 됩니다. 만약 이런 이름을 사용하면 해당 선언은 무시됩니다.

카운터 화면에 표시하기 (Displaying a counter)

카운터의 값은 content 속성 안에서 counter()counters() 함수를 사용해서 화면에 출력할 수 있습니다.

예를 들어, 아래의 코드는 counter()를 사용하여 각각의 <h3> 제목 앞에 Section <숫자>: 라는 텍스트를 붙여줍니다. 여기서 <숫자>는 십진수(기본 표시 스타일)로 출력된 카운터의 값입니다.

body {
  counter-reset: section; /* 'section'이라는 이름의 카운터를 설정하고 초기값을 0으로 잡습니다. */
}

h3::before {
  counter-increment: section; /* section 카운터 값을 1 증가시킵니다 */
  content: "Section " counter(section) ": "; /* 카운터 값을 기본 스타일(십진수)로 화면에 표시합니다 */
}

counter() 함수는 중첩된 레벨의 번호를 매길 때 부모 레벨의 맥락을 포함하지 않을 때 사용합니다.
예를 들어, 아래처럼 중첩된 각 레벨이 항상 1부터 다시 시작하는 경우입니다.

1 One
  1 Nested one
  2 Nested two
2 Two
  1 Nested one
  2 Nested two
  3 Nested three
3 Three

반면 counters() 함수는 중첩된 레벨의 번호에 부모 레벨의 번호까지 함께 포함시켜야 할 때 사용합니다.
보통 약관이나 목차에서 아래와 같은 섹션 레이아웃을 만들 때 자주 사용하시죠.

1 One
  1.1 Nested one
  1.2 Nested two
2 Two
  2.1 Nested one
  2.2 Nested two
  2.3 Nested three
3 Three

counter() 함수는 두 가지 형태를 가집니다: counter(<카운터-이름>) 그리고 counter(<카운터-이름>, <카운터-스타일>).
생성되는 텍스트는 해당 가상 요소 범위 내에서 가장 안쪽(innermost)에 있는 같은 이름의 카운터 값입니다.

counters() 함수 역시 두 가지 형태가 있습니다: counters(<카운터-이름>, <구분자-문자열>) 그리고 counters(<카운터-이름>, <구분자-문자열>, <카운터-스타일>).
생성되는 텍스트는 해당 가상 요소 범위 내에 있는 같은 이름의 모든 카운터 값들을 가장 바깥쪽부터 안쪽 순서로 나열하고, 지정한 <구분자-문자열>(예: ".")로 구분한 형태가 됩니다.

두 방식 모두 카운터는 지정된 <카운터-스타일>(기본값은 십진수 decimal)로 렌더링됩니다.
여러분은 list-style-type의 값 중 아무거나 사용하거나, 직접 커스텀 스타일(custom styles)을 만들어서 사용할 수도 있습니다.

counter()counters() 를 사용하는 구체적인 모습은 아래 기본 예제중첩 카운터 예제에서 확인해 보세요.

역순 카운터 (Reversed counters)

역순 카운터(Reversed counter)는 숫자가 올라가는(증가) 대신 내려가도록(감소) 의도된 카운터입니다.
역순 카운터를 만들려면 counter-reset에서 카운터 이름을 지을 때 reversed() 함수 기호를 사용하면 됩니다.

역순 카운터는 일반 카운터가 기본값 0을 가지는 것과 다르게, 대상 요소들의 총개수를 기본 초기값으로 가집니다.
덕분에 총 요소 개수부터 시작해서 1까지 카운트 다운하는 넘버링을 쉽게 구현할 수 있습니다.

예를 들어, 기본 초기값을 가지는 section이라는 이름의 역순 카운터를 만들려면 다음 문법을 사용합니다.

counter-reset: reversed(section);

물론 원하신다면 직접 아무 초기값이나 지정해 줄 수도 있습니다.
카운터 값을 줄일 때는 counter-increment 에 음수 값을 지정해서 감소시킵니다.

📝 참고:
역순 카운터가 아닌 일반 카운터에도 counter-increment에 음수를 넣어서 감소시킬 수 있습니다. 역순 카운터를 굳이 사용하는 가장 큰 장점은 "요소의 총개수를 자동으로 초기값으로 잡아준다는 것", 그리고 list-item 카운터가 역순 카운터일 경우 자동으로 값을 감소시켜 준다는 것입니다.

카운터 상속과 전파 (Counter inheritance and propagation)

각각의 요소나 가상 요소는 해당 요소의 스코프(범위) 내에 한 세트의 카운터들을 가집니다. 이 세트 안의 초기 카운터들은 요소의 부모(parent)와 바로 앞의 형제(preceding sibling) 요소로부터 받아옵니다. 카운터 값 자체는 이전 형제 요소의 마지막 자손, 이전 형제 요소 본인, 또는 부모로부터 전달받습니다.

어떤 요소가 카운터를 선언하게 되면, 그 카운터는 부모로부터 전달받은 같은 이름의 카운터 '안쪽(nested)'에 위치하게 됩니다. 만약 부모에게 같은 이름의 카운터가 없다면, 그냥 요소의 카운터 세트에 새롭게 추가됩니다. 반면, 이전 형제 요소로부터 전달받은 같은 이름의 카운터가 있다면 그건 카운터 세트에서 지워집니다.

counter() 함수는 제공된 이름과 일치하는 카운터 중 가장 안쪽(innermost) 에 있는 것을 가져옵니다. 반면 counters() 함수는 해당 이름으로 된 전체 카운터 트리를 모두 가져옵니다.

아래 예제에서는 primary라는 이름의 상속된 카운터와 secondary라는 이름의 형제 카운터가 어떻게 작동하는지 보여줍니다. 모든 <div> 요소는 counters() 함수를 써서 자신들의 카운터 값을 출력합니다. 주목할 점은 모든 카운터가 counter-reset 속성으로 생성되었고, 명시적으로 counter-increment를 써서 증가시킨 카운터는 하나도 없다는 것입니다.

HTML

(MDN Playground에서 실행해보기)

<section>
  counter-reset: primary 3
  <div>A</div>
  <div>B</div>
  <div>C</div>
  <div class="same-primary-name">D</div>
  <span> counter-reset: primary 6</span>
  <div>E</div>
  <div class="new-secondary-name">F</div>
  <span> counter-reset: secondary 5</span>
  <div>G</div>
  <div>H</div>
  <div class="same-secondary-name">I&nbsp;</div>
  <span> counter-reset: secondary 10</span>
  <div>J&nbsp;</div>
  <div>K</div>
  <section></section>
</section>

CSS

.same-primary-name,
.new-secondary-name,
.same-secondary-name {
  display: inline-block;
}

@counter-style style {
  system: numeric;
  symbols: "" "1" "2" "3" "4" "5" "6" "7" "8" "9" "10";
}
/* 부모 section에서 'primary' 카운터를 생성합니다 */
section {
  counter-reset: primary 3;
}

div::after {
  content: " ('primary' counters: " counters(primary, "-", style)
    ", 'secondary' counters: " counters(secondary, "-", style) ")";
  color: blue;
}

/* 새로운 'primary' 카운터를 중첩해서 생성합니다 */
.same-primary-name {
  counter-reset: primary 6;
}

/* 'F' div 요소에 'secondary' 카운터를 생성합니다 */
.new-secondary-name {
  counter-reset: secondary 5;
}

/* 형제 요소의 'secondary' 카운터를 덮어씁니다(override) */
.same-secondary-name {
  counter-reset: secondary 10;
}

부모 <section> 요소는 primary라는 카운터를 값 3으로 초기화하고, 자식인 모든 <div>들은 상속된 이 primary 카운터를 물려받습니다.
'D' 요소는 새롭게 primary(값 6) 카운터를 생성하는데, 이것은 부모로부터 물려받은 기존 primary 카운터 내부에 중첩(nested)됩니다. 따라서 'D' 이후로는 이름이 primary인 카운터가 값 36 두 개를 가지게 되죠.

'F' 요소는 최초로 secondary(값 5) 카운터를 생성하고, 이 카운터는 다음 형제인 'G'에게 전달됩니다. 'G'는 이를 다시 'H'에게 넘겨주죠. 그 후, 'I' 요소가 secondary라는 같은 이름으로 새로운 카운터(값 10)를 생성합니다. 이때 이전 형제인 'H'로부터 받았던 원래의 secondary(값 5) 카운터는 버려지고, 자신이 새로 만든 카운터를 다음 형제인 'J'에게 전달하게 됩니다.

counter-setcounter-reset의 차이점 (Difference between counter-set and counter-reset)

counter-set 속성은 이미 존재하고 있는 카운터를 업데이트합니다. (만약 해당 이름의 카운터가 아예 없다면 그때서야 새로 생성합니다.)
반면에 counter-reset 속성은 기존 카운터 존재 여부와 무관하게 항상 새로운 카운터를 생성(중첩)합니다.

다음 예제에는 부모 리스트 안에 두 개의 하위 리스트(sub-lists)가 있습니다. 모든 리스트 항목(li)은 'item'이라는 이름의 카운터를 사용해 번호가 매겨져 있습니다. 첫 번째 하위 리스트는 counter-set 속성을 사용하고, 두 번째 하위 리스트는 counter-reset 속성을 사용해서 'item' 카운터를 변경해 보았습니다.

HTML

(MDN Playground에서 실행해보기)

<ul class="parent">
  <li>A</li>
  <li>B</li>
  <li>
    C (counter-set으로 카운터를 업데이트함)
    <ul class="sub-list-one">
      <li>sub-A</li>
      <li>sub-B</li>
    </ul>
  </li>
  <li>D</li>
  <li>
    E (counter-reset으로 새 카운터를 생성함)
    <ul class="sub-list-two">
      <li>sub-A</li>
      <li>sub-B</li>
      <li>sub-C</li>
    </ul>
  </li>
  <li>F</li>
  <li>G</li>
</ul>

CSS

ul {
  list-style: none;
}
/* 처음으로 새 카운터를 생성합니다 */
.parent {
  counter-reset: item 0;
}

/* 각 리스트 아이템마다 카운터를 1씩 증가시킵니다 */
li {
  counter-increment: item;
}

/* 리스트 아이템에 숫자를 표시합니다 */
li::before {
  content: counter(item) " ";
}

/* 기존 카운터의 값을 10으로 강제 변경(업데이트)합니다 */
.sub-list-one {
  counter-set: item 10;
}

/* 아예 새롭게 카운터를 0부터 리셋(생성)합니다 */
.sub-list-two {
  counter-reset: item 0;
}

첫 번째 하위 리스트의 항목들이 11부터 번호를 부여받고, 그 번호가 다시 밖으로 빠져나와 부모 리스트(D 요소)로 쭉 이어지는 것을 주목해 보세요. 이는 counter-set 속성이 .parent 요소에서 선언된 원본 'item' 카운터 자체를 가져와서 업데이트했기 때문입니다.
반면, 두 번째 하위 리스트의 항목들은 '1'부터 새롭게 번호가 매겨지고, 그다음 부모 리스트 항목(F 요소)은 하위 리스트의 넘버링을 이어받지 않고 원래의 번호를 그대로 유지합니다. 이는 counter-reset 속성이 완전히 새로운 독립적인 카운터를 생성했고, 부모 리스트 항목들은 여전히 기존의 옛날 카운터를 바라보고 있기 때문입니다.

리스트 항목 카운터 (List item counters)

<ol> 요소를 사용해 만든 순서가 있는 리스트는, 기본적으로 암묵적인 list-item이라는 이름의 카운터를 갖고 있습니다.

다른 카운터들과 마찬가지로 증가 카운터일 때는 초기값이 0이고, 역순 카운터일 때는 "항목의 총개수"가 초기값이 됩니다.
개발자가 직접 만든 커스텀 카운터와 다른 점이 있다면, list-item 카운터는 요소가 추가될 때마다 리스트 요소 속성에 따라 자동으로 1씩 증가하거나 감소한다는 것입니다.

우리는 이 list-item 카운터를 조작해서 순서 있는 리스트의 기본 동작을 CSS만으로 입맛에 맞게 바꿀 수 있습니다.
예를 들어 기본 초기값을 변경하거나, counter-increment를 사용해 리스트 항목이 증가/감소하는 폭을 1이 아닌 다른 숫자로 바꿀 수도 있습니다.


예제 (Examples)

기본 예제 (Basic example)

이 예제는 모든 제목의 시작 부분에 "Section [카운터 값]:" 이라는 문구를 추가합니다.

CSS

(MDN Playground에서 실행해보기)

body {
  counter-reset: section; /* 'section'이라는 카운터를 설정하고 초기값을 0으로 맞춥니다. */
}

h3::before {
  counter-increment: section; /* section 카운터를 1 증가시킵니다 */
  content: "Section " counter(section) ": "; /* 'Section '이라는 단어와 section 카운터 값, 그리고 콜론(:)을 각 h3 내용 앞에 표시합니다. */
}

HTML

<h3>Introduction</h3>
<h3>Body</h3>
<h3>Conclusion</h3>

기본 예제: 역순 카운터 (Basic example: reversed counter)

이 예제는 방금 위에서 본 예제와 똑같지만, 역순 카운터(reversed counter)를 사용했다는 점만 다릅니다.
여러분의 브라우저가 reversed() 함수 표기법을 지원한다면 다음과 같이 카운트 다운되는 결과를 보실 수 있습니다.

reversed counter

CSS

(MDN Playground에서 실행해보기)

body {
  counter-reset: reversed(
    section
  ); /* 'section' 카운터를 역순으로 설정합니다. 초기값은 자동으로 요소의 총개수로 잡힙니다. */
}

h3::before {
  counter-increment: section -1; /* section 카운터 값을 1씩 감소시킵니다 */
  content: "Section " counter(section) ": "; 
}

HTML

<h3>Introduction</h3>
<h3>Body</h3>
<h3>Conclusion</h3>

좀 더 복잡한 예제 (A more sophisticated example)

카운터를 값을 증가시킬 때마다 반드시 화면에 보여주어야만 하는 건 아닙니다.
이 예제에서는 문서 내의 모든 링크(a 태그) 개수를 세되, 사용자의 편의를 위해 텍스트 내용이 텅 비어있는 링크 요소에 한해서만 번호를 달아주는 방식으로 활용해 보았습니다.

CSS

(MDN Playground에서 실행해보기)

:root {
  counter-reset: link;
}

a[href] {
  counter-increment: link; /* 모든 링크에서 카운터는 증가합니다 */
}

a[href]:empty::after {
  content: "[" counter(link) "]"; /* 하지만 내용이 비어있는(empty) 링크 뒤에만 번호를 출력합니다 */
}

HTML

<p>See <a href="[https://www.mozilla.org/](https://www.mozilla.org/)" aria-label="Mozilla"></a></p>
<p>Do not forget to <a href="contact-me.html">leave a message</a>!</p>
<p>See also <a href="[https://developer.mozilla.org/](https://developer.mozilla.org/)" aria-label="MDN"></a></p>

중첩 카운터 예제 (Example of a nested counter)

CSS 카운터는 개요(목차) 형태의 리스트를 만들 때 그 진가를 발휘합니다. 자식 요소로 깊어질 때마다 카운터의 새로운 인스턴스가 자동으로 생성되기 때문이죠.
counters() 함수를 사용하면, 각기 다른 중첩 레벨 사이에 원하는 문자열(구분자)을 삽입할 수 있습니다.

CSS

(MDN Playground에서 실행해보기)

ol {
  counter-reset: section; /* 새로운 ol 요소를 만날 때마다 새로운 section 카운터 인스턴스를 생성합니다 */
  list-style-type: none;
}

li::before {
  counter-increment: section; /* 현재 컨텍스트의 section 카운터 인스턴스만 1 증가시킵니다 */
  content: counters(section, ".") " "; /* 현재까지 쌓인 모든 section 카운터 인스턴스들을 마침표(.)로 연결해서 출력합니다 */
}

HTML

<ol>
  <li>item</li>          <li>item               <ol>
      <li>item</li>      <li>item</li>      <li>item           <ol>
          <li>item</li>  <li>item</li>  </ol>
        <ol>
          <li>item</li>  <li>item</li>  <li>item</li>  </ol>
      </li>
      <li>item</li>      </ol>
  </li>
  <li>item</li>          <li>item</li>          </ol>
<ol>
  <li>item</li>          <li>item</li>          </ol>

사양 (Specifications)

Specification
CSS Lists and Counters Module Level 3
# auto-numbering

같이 보기 (See also)


MDN 개선에 참여하기 (Help improve MDN)

이 페이지가 도움이 되셨나요? [네 (Yes)] / [아니요 (No)]

기여하는 방법 알아보기 (Learn how to contribute)

이 페이지는 2025년 12월 16일에 MDN 기여자들 (MDN contributors)에 의해 마지막으로 수정되었습니다.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글