목표: CSS 선택자의 종류와 우선순위(특이성)를 이해하고, 폼·테이블·카드 레이아웃 등 실무에서 바로 쓰는 패턴을 예제로 익힙니다.
1) 기본 선택자
| 종류 | 예시 | 설명 |
|---|---|---|
| 전체 선택자 | * { margin:0; } |
모든 요소에 적용 |
| 태그(타입) | p { ... } |
특정 태그에 적용 |
| 클래스 | .card { ... } |
재사용 가능한 그룹 |
| 아이디 | #main { ... } |
문서 내 유일(고유) 요소 |
| 그룹화 | h1, h2, h3 { ... } |
여러 선택자에 동일 규칙 |
예제
<!-- HTML -->
<div class="card">
<h3 id="title">Hello</h3>
<p>본문입니다.</p>
</div>
<!-- CSS -->
* { box-sizing: border-box; } /* 전체 */
.card { border:1px solid #e2e8f0; } /* 클래스 */
#title { color:#2563eb; } /* 아이디 */
p { margin:0; } /* 태그 */
h3, p { font-family: system-ui; } /* 그룹 */
미리보기
Hello
본문입니다.
2) 결합자: 요소 간 관계 선택
| 결합자 | 예시 | 설명 |
|---|---|---|
| 공백(자손) | .card p |
.card 내부의 모든 p(깊이 무관) |
| > (자식) | .list > li |
.list의 직계 자식 li만 |
| + (인접 형제) | h3 + p |
h3 바로 다음 p 하나 |
| ~ (일반 형제) | h3 ~ p |
h3 뒤에 나오는 모든 p 형제 |
예제
<!-- HTML -->
<div class="card">
<h3>제목</h3>
<p>이 문단은 h3 바로 다음(+)</p>
<div><p>이 문단은 자손(공백)</p></div>
<p>이 문단은 형제들 중 하나(~)</p>
</div>
<!-- CSS -->
.card p { color:#0f766e; } /* 자손 */
.card > p { font-weight:700; } /* 자식 */
h3 + p { text-decoration:underline; } /* 인접 형제 */
h3 ~ p { font-style:italic; } /* 일반 형제 */
미리보기
제목
이 문단은 h3 바로 다음(+)
이 문단은 자손(공백)
이 문단은 형제들 중 하나(~)
3) 속성 선택자
| 형태 | 예시 | 의미 |
|---|---|---|
[attr] |
input[required] |
속성이 존재 |
[attr="v"] |
a[target="_blank"] |
정확히 일치 |
[attr^="v"] |
img[src^="https://"] |
v로 시작 |
[attr$="v"] |
img[src$=".png"] |
v로 끝남 |
[attr*="v"] |
a[href*="docs"] |
v 포함 |
[attr~="v"] |
[class~="active"] |
공백 분리된 단어 중 v |
[attr|="v"] |
[lang|="en"] |
v 또는 v-로 시작 |
예제
<!-- HTML -->
<input required placeholder="이름" />
<img src="https://example.com/photo.png" alt="샘플">
<a href="/docs/intro" target="_blank">문서</a>
<!-- CSS -->
input[required] { border:2px solid #f59e0b; }
img[src$=".png"] { outline:3px dashed #22c55e; }
a[href*="docs"][target="_blank"] { color:#2563eb; }
미리보기
4) 의사 클래스 (상태/위치)
:hover,:active,:focus: 상호작용 상태:first-child,:last-child,:nth-child(n): 형제 내 위치:checked,:disabled,:valid,:invalid: 폼 상태:not(): 부정,:is()/:where(): 여러 선택자 그룹:has(): “부모가 자식/후손을 가지면” 선택 (관계 선택)
예제
<!-- HTML -->
<button class="btn">버튼</button>
<ul class="menu">
<li>하나</li> <li>둘</li> <li>셋</li>
</ul>
<input type="email" class="field" placeholder="이메일" required />
<!-- CSS -->
.btn:hover { transform: translateY(-2px); }
.menu li:nth-child(2) { color:#ef4444; }
.field:valid { outline:2px solid #22c55e; }
.field:invalid { outline:2px solid #ef4444; }
.menu li:not(:first-child) { font-style: italic; }
미리보기
- 하나
- 둘 (nth-child(2))
- 셋 (not(:first-child))
※ 이메일 형식이면 초록, 아니면 빨강 아웃라인
5) 의사 요소 (::before, ::after, ::first-line, ::selection)
요소 내부의 특정 “가상 부분”을 선택합니다. ::before/::after를 쓰려면 content가 필요합니다.
예제
<!-- HTML -->
<p class="quote">배움은 끝이 없다.</p>
<!-- CSS -->
.quote::before { content:"“"; color:#2563eb; font-size:2rem; margin-right:.2rem; }
.quote::after { content:"”"; color:#2563eb; font-size:2rem; margin-left:.2rem; }
.quote::first-line { font-weight:700; }
::selection { background:#fde68a; color:#111; }
미리보기
“배움은 끝이 없다.”
텍스트를 드래그해보면 selection 색이 바뀌는 효과(브라우저별 다를 수 있음)
6) 특이성(우선순위) & :is / :where / :not / :has 정리
| 선택자 | 특이성 가중치(개념) | 비고 |
|---|---|---|
| ID 선택자(#id) | 100 | 가급적 과사용 금지 |
| 클래스/속성/의사클래스(.cls, [a], :hover) | 10 | 주력 |
| 태그/의사요소(p, ::before) | 1 | 가벼움 |
| 전체선택자(*), 결합자(공백, >, +, ~) | 0 | 특이성 없음 |
| :is(…) | 괄호 내부에서 가장 높은 값 | 가독성↑ |
| :where(…) | 항상 0 | 리셋/스코프에 유용 |
| :not(…) | 괄호 외부 선택자의 특이성만 | 내부는 추가X |
| :has(…) | 괄호 내부에서 가장 높은 값 | 관계 선택 |
팁: 컴포넌트 스타일링은 .wrapper :where(h1, h2, p, a)처럼 :where를 쓰면 특이성 낮게 유지 → 나중에 오버라이드 쉬움.
예제
/* 스코프 낮추기 */
.card :where(h1, h2, p, a) { margin:0; }
/* 읽기 쉬운 다형 선택 */
.btn:is(.primary, .danger):hover { transform: translateY(-2px); }
/* 상태 부정 */
.input:not(:focus) { opacity:.9; }
/* 부모가 특정 자식/후손을 가질 때 */
.list:has(li.selected) { border-color:#22c55e; }
미리보기
- 항목1
- 항목2(selected)
※ 실제 :has 적용 결과는 브라우저에서 렌더링 시 반영됩니다.
7) 실무 패턴 5가지
패턴 A. 지그재그(줄무늬) 테이블
<table class="zebra">
<thead><tr><th>#</th><th>이름</th><th>점수</th></tr></thead>
<tbody>
<tr><td>1</td><td>Kim</td><td>92</td></tr>
<tr><td>2</td><td>Lee</td><td>81</td></tr>
<tr><td>3</td><td>Park</td><td>88</td></tr>
</tbody>
</table>
<style>
.zebra tbody tr:nth-child(even) { background:#f8fafc; }
.zebra tbody tr:hover { background:#eef2ff; }
</style>
미리보기
| # | 이름 | 점수 |
|---|---|---|
| 1 | Kim | 92 |
| 2 | Lee | 81 |
| 3 | Park | 88 |
패턴 B. 폼 상태 스타일 (required/valid/invalid)
<input class="ctl" type="email" placeholder="이메일" required>
<style>
.ctl:required { border:1px solid #94a3b8; }
.ctl:valid { outline:2px solid #22c55e; }
.ctl:invalid { outline:2px solid #ef4444; }
.ctl:focus { box-shadow:0 0 0 4px #dbeafe; }
</style>
미리보기
패턴 C. 카드 hover와 인접 형제
<div class="card2">
<h4>타이틀</h4>
<p>설명...</p>
</div>
<style>
.card2 { border:1px solid #e2e8f0; border-radius:12px; padding:12px; transition:.2s; }
.card2:hover { transform: translateY(-4px); box-shadow:0 10px 20px rgba(0,0,0,.06); }
.card2 h4 + p { color:#475569; } /* h4 바로 다음 문단 */
</style>
미리보기
타이틀
설명...
패턴 D. 탭 UI (라디오 :checked + 형제 결합자)
<div class="tabs">
<input type="radio" id="t1" name="tabs" checked>
<label for="t1">탭1</label>
<input type="radio" id="t2" name="tabs">
<label for="t2">탭2</label>
<div class="panel p1">패널 1</div>
<div class="panel p2">패널 2</div>
</div>
<style>
.tabs input { display:none; }
.tabs label { padding:.4rem .8rem; border:1px solid #cbd5e1; border-bottom:none; cursor:pointer; }
.tabs .panel { display:none; border:1px solid #cbd5e1; padding:10px; }
#t1:checked ~ .p1, #t2:checked ~ .p2 { display:block; }
#t1:checked + label, #t2:checked + label { background:#eef2ff; }
</style>
미리보기
패턴 E. 부모 조건 스타일(:has)
<ul class="list">
<li>A</li>
<li class="selected">B</li>
</ul>
<style>
.list:has(.selected) { border:2px solid #22c55e; }
.list li.selected { font-weight:700; color:#16a34a; }
</style>
미리보기
- A
- B (selected)
8) 연습문제
- 결합자: 카드(.card) 안에서
h3다음에 오는p만 밑줄 긋기 (+사용) - 속성 선택자:
a[target="_blank"]에 아이콘 붙이기(::after와content) - 의사 클래스: 리스트 2,4,6번째 항목만 배경 색칠 (
:nth-child(2n)) - 부정:
.btn중.danger가 아닌 버튼만 파란 테두리 (:not(.danger)) - :where: 래퍼
.article내부의h1, h2, p공통 여백을 특이성 0으로 초기화
정답은 위 섹션 예제들을 조합해 스스로 만들어보세요. 특이성이 꼬이면 :where()로 낮추고, 모듈별 래퍼 클래스를 꼭 쓰는 습관을 들이면 좋아요.
