AI Animation
절제된 UI 속 AI 결과에 짧은 임팩트를 남기는 회전형 그라데이션 테두리
긴 문서에서 반복되는 내용을 덜어내고 핵심 흐름을 세 문장으로 정리했습니다.
핵심 내용
문제의 배경과 목표가 명확하게 연결되어 있어요
결론을 뒷받침하는 근거가 세 가지 있어요
더 살펴볼 내용
본문에서 인용한 수치의 출처를 확인해 보세요.
요즘 제품 UI는 색을 절제하고 단순한 면과 얇은 경계로 정리되는 경우가 많습니다. 기존 기능에는 잘 어울리지만 AI 결과까지 같은 톤으로 맞추면 화면의 다른 카드와 같은 무게로 보였습니다.
그라디언트는 한 면 안에서 색과 방향의 변화를 함께 보여줄 수 있습니다. 하지만 카드 전체를 채우면 내용보다 강해집니다. 그래서 안쪽의 모던한 톤은 유지하고, 결과가 도착하는 순간에만 가장자리의 그라디언트 라인으로 임팩트를 주었습니다.
그라디언트 라인은 border로 만들기 어렵다
border-color에는 그라데이션을 넣을 수 없습니다. border-image는 그라데이션을 표현할 수 있지만, 둥근 모서리를 따라 하나의 빛을 회전시키고 블러까지 적용하기는 어렵습니다.
필요한 것은 네 개의 선이 아니라 카드 뒤에서 회전하는 하나의 평면이었습니다. 그래서 테두리를 직접 그리지 않고 conic-gradient 배경을 만들었습니다. 아래에서 회전을 멈춰보면 색이 있는 짧은 구간과 나머지 투명한 영역을 확인할 수 있습니다.
배경 위에 카드를 올려 선으로 만든다
실제 카드에서는 그라데이션 배경 위를 불투명한 요소로 덮습니다. 부모의 2px 여백으로 드러난 배경만 테두리처럼 보입니다.
Stack을 오른쪽으로 옮기면 카드가 z축을 따라 내려가며 배경 위에 포개집니다.
.frame {
position: relative;
isolation: isolate;
contain: paint;
padding: 2px;
border-radius: 18px;
}
.frame::before {
content: "";
position: absolute;
z-index: 0;
left: 50%;
top: 50%;
width: 200%;
height: 200%;
}
.card {
position: relative;
z-index: 1;
border-radius: 16px;
background: var(--surface);
}
레이어를 분리했기 때문에 ::before만 회전하고 카드의 내용은 움직이지 않습니다. 배경은 모서리가 비지 않도록 크게 만들고 contain: paint로 바깥 라운드 안에서 자릅니다. 블러도 배경에만 적용되어 글자와 아이콘은 선명하게 남습니다.
짧은 빛이 한 바퀴 돌고 선으로 채워진다
conic-gradient는 중심을 기준으로 색의 구간을 나눕니다.
background: conic-gradient(
var(--lead) 0deg 60deg,
var(--trail) 60deg var(--fill-angle),
transparent var(--fill-angle) 360deg
);
0deg부터 --fill-angle까지만 색이 있고 이후는 투명합니다. 값이 90deg일 때는 짧은 호만 보입니다. 이 평면을 한 바퀴 돌린 뒤 값을 360deg까지 늘리면 투명한 영역이 사라지고 같은 빛이 완성된 테두리로 남습니다.
그라디언트의 각도를 움직이려면
짧은 호를 완성된 선으로 만들려면 그라디언트의 경계를 90deg에서 360deg까지 움직여야 합니다. 어떻게 애니메이션할 수 있을까요?
가장 익숙한 방법은 keyframes에 시작과 끝의 background-image를 넣는 것입니다.
@keyframes fill {
from {
background-image: conic-gradient(
var(--line) 0deg 90deg,
transparent 90deg 360deg
);
}
to {
background-image: conic-gradient(
var(--line) 0deg 360deg,
transparent 360deg
);
}
}
하지만 이 방법으로는 각도가 움직이지 않습니다. conic-gradient()는 하나의 배경 이미지이고, background-image는 두 이미지 사이의 중간 상태를 만들지 않기 때문입니다. 브라우저는 두 그라디언트 안의 색상 경계가 같은 값의 이동이라는 사실을 알 수 없어 시작 이미지에서 끝 이미지로 바로 전환합니다.
이런 경우 @property를 사용할 수 있습니다. 배경 이미지 전체가 아니라 실제로 변해야 하는 각도만 --fill-angle로 분리하고, 이 값이 각도라는 사실을 브라우저에 알려주는 문법입니다.
@property --fill-angle {
syntax: "<angle>";
inherits: false;
initial-value: 90deg;
}
syntax: "<angle>"은 값을 각도로 등록합니다. inherits: false는 진행 각도를 자식에게 물려주지 않고, initial-value는 별도 값이 없을 때 짧은 호로 시작하게 합니다.
이제 keyframe은 이미지가 아니라 각도 하나를 움직입니다. 브라우저가 매 프레임 중간 각도를 계산하고, 그 값이 들어간 conic-gradient를 다시 그립니다.
@keyframes fill {
from {
--fill-angle: 90deg;
}
to {
--fill-angle: 360deg;
}
}
두 방식을 같은 시간으로 재생해 보세요. 왼쪽은 배경 이미지를 한 번에 바꾸고, 오른쪽은 각도의 중간값을 따라 테두리를 채웁니다.
background-image
@property
실제 효과에서는 이 채우기를 배경의 회전 뒤에 이어 붙였습니다.
.frame::before {
animation:
rotate 1.2s forwards,
fill 0.8s forwards 1.2s;
}
rotate는 바로 시작해 배경의 transform을 바꿉니다. fill은 1.2초 뒤에 시작해 --fill-angle을 채웁니다. 서로 다른 속성을 움직이므로 같은 ::before에서 이어서 실행할 수 있습니다. 아래 타임라인에서 두 단계의 경계를 확인해 보세요.
긴 문서에서 반복되는 내용을 덜어내고 핵심 흐름을 세 문장으로 정리했습니다.
핵심 내용
문제의 배경과 목표가 명확하게 연결되어 있어요
결론을 뒷받침하는 근거가 세 가지 있어요
더 살펴볼 내용
본문에서 인용한 수치의 출처를 확인해 보세요.
완료 뒤에는 멈춘다
이 움직임은 로딩이 아니라 결과의 도착을 알리는 전환입니다. 처음 나타날 때와 다시 보기에서만 한 번 재생합니다. 모션 감소 설정에서는 애니메이션을 건너뛰고 완성된 테두리와 내용을 바로 보여줍니다.