고급스러운 유리 질감 표현하기
Uiverse의 Generate 버튼을 하나씩 분해하며, 가상 요소와 box-shadow, animation-delay, position이 실제 UI 안에서 어떤 역할을 하는지 정리한 글
https://uiverse.io/dexter-st/young-newt-20
핵심
이 컴포넌트는 3장의 레이어를 겹쳐서 만들었다.
| 레이어 | 정체 | 역할 |
|---|---|---|
::before | 버튼보다 4px 큰 유령 레이어 (뒤) | 바깥 테두리 + 아래에서 올라오는 빛 |
.btn | 진짜 버튼 | 유리 질감 본체 |
::after | 버튼과 같은 크기 (위) | 파란 네온 오버레이 |
레이어 구조 특징
-
.btn에는position속성이 없다. -
그래서
::before의position: absolute는.btn-wrapper를 기준으로 잡힌다.<div class="btn-wrapper"> <button class="btn"> ....btn::before { ... } .btn::after { ... } -
wrapper가
inline-block이라 버튼 크기에 딱 맞고, 결과적으로width: calc(100% + 8px)가 정확히 "버튼보다 4px씩 큰 액자"가 된다. -
wrapper가 없으면 이 구조가 통째로 무너지게 된다.
왜 가상요소인가
-
장식은 DOM에 존재할 필요가 없기 때문이다.
-
가상요소 없이도 만들 수 있다.
<button class="btn"> <div class="bezel"></div> <!-- 바깥 테두리 --> <div class="glow"></div> <!-- 파란 오버레이 --> <svg>...</svg> <div class="txt-wrapper">...</div> </button> -
하지만 버튼 안에 튼 안에 아무 의미 없는 div 두 개가 들어간다.
- 문서의 원래 의미를 파악하기 힘들어진다.
- 불필요한 div가 접근성을 해치지는 않지만 시각적 꾸밈은 CSS에게 역할을 맡기는게 합리적인 패턴이다.
-
가상요소로 구현한 기능을 시각적 차이를 보면 다음과 같다.
유리 질감 특징
inset 0px 1px 1px rgba(255,255,255,0.2)
inset 0px 2px 2px rgba(255,255,255,0.15)
inset 0px 4px 4px rgba(255,255,255,0.1)
...
-
1 → 2 → 4 → 8 → 16px으로 두 배씩 키우면서 투명도는 낮춘다. -
이는 실제 빛의 감쇠 곡선을 흉내 내기 위한 기법이다.
-
그림자 하나로
0 16px 16px만 쓰면 밋밋한 뿌연 덩어리가 되는데, 여러 겹을 쌓으면 가장자리는 선명하고 안쪽은 부드럽게 퍼지는 진짜 유리 광택이 나온다.한 겹다섯 겹 -
그리고 바깥 그림자는 y값이 전부 음수이다.
-
보통 그림자는 아래로 지는데, 이건 위쪽으로 지게 만들었다.
-
광원이 아래에 있다는 뜻이고, 그래서 "아래에서 빛을 받는 유리판" 느낌이 난다.
y 양수 · 아래로 진다y 음수 · 위로 진다
글자가 흘러가는 원리
-
버튼에는 Generate 글자가 순서대로 빛나는 효과가 있다.
-
이는
Generate를 8개의<span>으로 쪼갠 뒤에 각 글자에 스타일을 넣어 구현했다. 가장 신박하다고 느낀 부분이다.<!-- 글자 하나하나를 span으로 분리해야 개별 delay를 줄 수 있음 --> <div class="txt"> <span class="btn-letter">G</span> <span class="btn-letter">e</span> <span class="btn-letter">n</span> <span class="btn-letter">e</span> <span class="btn-letter">r</span> <span class="btn-letter">a</span> <span class="btn-letter">t</span> <span class="btn-letter">e</span> </div> ....txt { /* span을 줄바꿈해서 쓰면 태그 사이 공백이 실제 띄어쓰기로 렌더링된다. 그 공백을 상쇄해서 글자가 붙어 보이게 하는 트릭 */ word-spacing: -1em; } .btn-letter { display: inline-block; /* inline이면 transform/scale이 안 먹으므로 필수 */ color: #fff5; /* 평소 상태: 알파 5/15 정도의 흐린 흰색 = "꺼진 글자" */ /* 2초 주기로 무한 반복. 아래 nth-child delay와 합쳐져 물결처럼 흐름 */ animation: letter-anim 2s ease-in-out infinite; transition: color 0.4s, text-shadow 0.4s; } /* 0% / 100%는 생략 → 자동으로 원래 상태(#fff5, 그림자 없음)에서 시작·복귀. 중간 지점에서만 밝아지므로 "켜졌다 꺼지는" 왕복 운동이 됨 */ @keyframes letter-anim { 50% { color: #fff; /* 완전한 흰색으로 점등 */ text-shadow: 0 0 3px #fff8; /* 은은한 발광(glow) */ } } /* 핵심: 글자마다 0.08초씩 시작을 늦춤. 전체 지연(8글자 × 0.08 = 0.64s)이 애니메이션 주기(2s)보다 짧아야 끊기지 않고 왼쪽→오른쪽으로 흐르는 것처럼 보임 */ .btn-letter:nth-child(1) { animation-delay: 0s; } .btn-letter:nth-child(2) { animation-delay: 0.08s; } .btn-letter:nth-child(3) { animation-delay: 0.16s; } .btn-letter:nth-child(4) { animation-delay: 0.24s; } .btn-letter:nth-child(5) { animation-delay: 0.32s; } .btn-letter:nth-child(6) { animation-delay: 0.40s; } .btn-letter:nth-child(7) { animation-delay: 0.48s; } .btn-letter:nth-child(8) { animation-delay: 0.56s; } -
애니메이션 자체는 모든 글자가 동일하지만, 시작 시각을 다르게 하여 바뀌는 듯한 시각 효과를 주었다.
G 0s e 0.08s n 0.16s e 0.24s .../* Animation delays for .btn-letter elements */ .btn-letter:nth-child(1), .btn:focus .btn-letter:nth-child(1) { animation-delay: 0s; } .btn-letter:nth-child(2), .btn:focus .btn-letter:nth-child(2) { animation-delay: 0.08s; } .btn-letter:nth-child(3), .btn:focus .btn-letter:nth-child(3) { animation-delay: 0.16s; } ... -
0.08초씩 밀린 같은 애니메이션이 나란히 놓이면, 밝아지는 지점이 왼쪽에서 오른쪽으로 이동하는 것처럼 보인다.
-
CSS만으로 텍스트 일부에 시차를 줄 방법이 없어서 span으로 쪼갠 것이다.
기타 CSS 문법 공부해보기
1. position: absolute 에 대하여
-
position: absolute를 쓴 요소는 반드시 기준점이 될 조상을 하나 찾아야 한다. -
규칙은 딱 하나다. 부모부터 위로 올라가면서
position이static이 아닌 첫 번째 조상을 기준으로 삼는다. 끝까지 없으면 페이지 전체가 기준. -
참고로
position: static은 모든 요소의 기본값이다. 즉 아무것도 안 쓴 요소는 넘어간다. -
이 버튼의 구조는 다음과 같다.
.btn-wrapper → position: relative ✅ 기준 .btn → (없음) = static ❌ 기준 아님 ::before → position: absolute "기준 어디?" -
::before부모.btn는static이기 때문에 넘어가고,.btn-wrapper를 기준으로 삼는다. -
왜 이렇게 구현했을까?
.btn에position: relative를 추가하면 2px이 어긋나게 된다. -
%크기를 계산할 때 기준이 되는 영역이 다르기 때문이다.기준 width: 100%가 가리키는 것.btn-wrapper(현재)버튼의 테두리 바깥까지 .btn(relative 추가 시)버튼의 테두리 안쪽까지 -
.btn에는border: 1px이 있어서 좌우 합쳐 2px 차이가 난다.