GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

css 텍스트 그림자 효과 9가지 — 층 값이 입체를 만든다

css 텍스트 그림자 효과는 text-shadow 값 하나를 층 문자열로 계산해 조립하는 아홉 가지 패턴이다. 그림자는 빛이 사라진 자리인데, 이 아홉 글자는 그림자만으로 빛을 대신한다 — 01번은 겹친 층 값으로 입체를 세우고, 05번은 어긋난 색 값으로 흔들리게 하며, 09번은

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

css 텍스트 그림자 효과 9가지 — 값 하나가 그림자를 어떻게 바꾸는가

아홉 글자 모두 CSS 속성 자체는 text-shadow 하나뿐이다. 화면에 남는 인상을 가르는 건 그 뒤에서 도는 JS 값 하나 — 층 개수·오프셋 거리·색 분리 폭·스크롤 진행률 같은 수치가 프레임마다 그림자 문자열을 다시 조립한다. 아래 아홉 개는 그 값이 어디에 꽂히는지를 하나씩 다르게 보여준다.

01층 값이 쌓여 입체가 되는 돌출 글자

제목처럼 쓰는 글자가 대각선 아래로 층을 매달고 텍스트 3d 효과처럼 두꺼워 보인다. buildExtrudeShadow 함수는 SHADOW_LAYER_COUNT(6)개의 그림자를 step×층 번호만큼 쌓고 extrudeColor 로 층마다 명도를 24%에서 6%까지 낮춰, 1px~15px 사이를 오가는 step 값이 돌출 깊이를 정한다.

층 값이 쌓여 입체가 되는 돌출 글자
새 창에서 열기

01 · 층 값이 쌓여 입체가 되는 돌출 글자 — 측정 누적면적 10.384% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SHADOW_LAYER_COUNTbuildExtrudeShadowrenderExtrude
  // 계산 레이어: 층 번호(i)가 깊어질수록 명도를 낮춰 아래로 갈수록 어두워지는 입체감을 만든다
  function extrudeColor(i, n) {
    var t = i / n;
    var l = Math.max(6, Math.round(24 - t * 16));
    return 'hsl(232 14% ' + l + '%)';
  }

  // 값(step) 하나로 SHADOW_LAYER_COUNT 개의 그림자를 문자열로 조립한다
  function buildExtrudeShadow(step) {
    var parts = [];
    for (var i = 1; i <= SHADOW_LAYER_COUNT; i++) {
      var d = (i * step).toFixed(2);
      parts.push(d + 'px ' + d + 'px 0 ' + extrudeColor(i, SHADOW_LAYER_COUNT));
    }
    return parts.join(', ');
  }

01 항목은 누적면적 10.4%·강도 평균 26.0·23프레임 중 22프레임에서 움직였다.

02밝은 값과 어두운 값이 반대로 겹쳐 도드라지는 글자

글자 엠보싱처럼 둘레에 흰 그림자와 검은 그림자가 서로 반대편에 붙어, 같은 배경 위에서 도드라지거나 파인 듯 보인다. buildEmbossShadow 함수는 각도 값(angleDeg) 하나로 코사인·사인을 구해 EMBOSS_DEPTH_PX(12.5px)만큼 밝은 그림자와 어두운 그림자를 정반대 쪽에 배치하고, 각도가 0~360도로 도는 동안 요철 방향도 함께 돈다.

밝은 값과 어두운 값이 반대로 겹쳐 도드라지는 글자
새 창에서 열기

02 · 밝은 값과 어두운 값이 반대로 겹쳐 도드라지는 글자 — 측정 누적면적 10.903% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EMBOSS_DEPTH_PXbuildEmbossShadowrenderEmboss
  // 값(angleDeg) 하나로 밝은 그림자·어두운 그림자를 서로 반대 방향에 배치한다
  function buildEmbossShadow(angleDeg) {
    var rad = (angleDeg * Math.PI) / 180;
    var lx = (Math.cos(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    var ly = (Math.sin(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    var dx = (-Math.cos(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    var dy = (-Math.sin(rad) * EMBOSS_DEPTH_PX).toFixed(2);
    return [
      lx + 'px ' + ly + 'px 0 rgba(255,255,255,.85)',
      dx + 'px ' + dy + 'px 1px rgba(10,10,14,.75)'
    ].join(', ');
  }

02 항목은 누적면적 10.9%·강도 평균 129.2·23프레임 중 23프레임에서 움직였다.

03여덟 방향 값이 겹쳐 윤곽선이 되는 글자

채움 없는 숫자 둘레에 그림자만 여덟 방향으로 퍼져 윤곽선처럼 보인다. buildOutlineShadow 함수는 값(d) 하나를 DIRS 배열의 여덟 방향 벡터에 곱해 상하좌우·대각선에 같은 거리만큼 겹치고, d 가 0.4px~15px 사이를 오가며 윤곽 두께를 바꾼다.

여덟 방향 값이 겹쳐 윤곽선이 되는 글자
새 창에서 열기

03 · 여덟 방향 값이 겹쳐 윤곽선이 되는 글자 — 측정 누적면적 10.6% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

OUTLINE_MAX_PXbuildOutlineShadowrenderOutline
  var DIRS = [
    [1, 0], [-1, 0], [0, 1], [0, -1],
    [0.7071, 0.7071], [-0.7071, 0.7071], [0.7071, -0.7071], [-0.7071, -0.7071]
  ];

  var word = document.getElementById('olWord');
  var value = document.getElementById('olValue');
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 값(d) 하나를 여덟 방향에 같은 거리만큼 겹쳐 채움 없이도 윤곽선만 남긴다
  function buildOutlineShadow(d) {
    var parts = [];
    for (var i = 0; i < DIRS.length; i++) {
      var x = (DIRS[i][0] * d).toFixed(2);
      var y = (DIRS[i][1] * d).toFixed(2);
      parts.push(x + 'px ' + y + 'px 0 rgba(23,20,26,.92)');
    }
    return parts.join(', ');
  }

03 항목은 누적면적 10.6%·강도 평균 140.2·23프레임 중 21프레임에서 움직였다.

04대각선 길이 값만큼 길게 늘어지는 복고풍 글자

레트로 포스터에서 보던 것처럼 글자 오른쪽 아래로 그림자가 길게 늘어진다. buildRetroShadow 함수는 값(len)을 반올림한 정수만큼 1px씩 대각선으로 쌓고 층마다 투명도를 0.9에서 0.35만큼 낮춰, 4px~34px 사이를 오가는 len 값이 그림자 길이를 정한다.

대각선 길이 값만큼 길게 늘어지는 복고풍 글자
새 창에서 열기

04 · 대각선 길이 값만큼 길게 늘어지는 복고풍 글자 — 측정 누적면적 10.081% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RETRO_MAX_LENbuildRetroShadowrenderRetro
  // 값(len) 하나를 대각선 방향으로 한 칸(1px)씩 반복해 길이만큼 길게 늘어뜨린다
  function buildRetroShadow(len) {
    var parts = [];
    var n = Math.round(len);
    for (var i = 1; i <= n; i++) {
      parts.push(i + 'px ' + i + 'px 0 rgba(10,79,232,' + (0.9 - (i / n) * 0.35).toFixed(2) + ')');
    }
    return parts.join(', ');
  }

04 항목은 누적면적 10.1%·강도 평균 140.3·23프레임 중 22프레임에서 움직였다.

05색 분리 값만큼 어긋나며 깨지는 글리치 글자

빨간 그림자와 파란 그림자가 서로 반대 방향으로 벌어져 방송 신호가 어긋난 것처럼 보인다. buildGlitchShadow 함수는 값(mag)을 seedRand 고정 시드 난수에 곱해 두 색을 반대로 흔들고, GLITCH_SPLIT_PX(2px) 기준 0.3배~9.8배로 커지는 mag 값이 클릭마다 시드를 바꿔 다시 깨진다.

색 분리 값만큼 어긋나며 깨지는 글리치 글자
새 창에서 열기

05 · 색 분리 값만큼 어긋나며 깨지는 글리치 글자 — 측정 누적면적 10.158% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLITCH_SPLIT_PXseedRandbuildGlitchShadow
  // 고정 시드 PRNG — Math.random 대신 프레임마다 같은 규칙으로 다음 값을 만든다
  function seedRand(s) {
    return function () {
      s = (s * 1103515245 + 12345) & 0x7fffffff;
      return s / 0x7fffffff;
    };
  }
  var rnd = seedRand(seed);

  // 값(mag) 하나를 시드 노이즈로 흔들어 빨강·파랑 그림자를 반대 방향으로 어긋나게 한다
  function buildGlitchShadow(mag) {
    var rx = (mag * (rnd() * 2 - 1)).toFixed(2);
    var ry = (mag * 0.28 * (rnd() * 2 - 1)).toFixed(2);
    var bx = (-mag * (rnd() * 2 - 1)).toFixed(2);
    var by = (-mag * 0.28 * (rnd() * 2 - 1)).toFixed(2);
    return [
      rx + 'px ' + ry + 'px 0 rgba(255,54,96,.88)',
      bx + 'px ' + by + 'px 0 rgba(58,164,255,.88)'
    ].join(', ');
  }

05 항목은 누적면적 10.2%·강도 평균 144.0·23프레임 중 23프레임에서 움직였다.

06밝기 값이 층마다 달라 광택이 도는 금속 글자

위는 밝고 아래는 어두운 금속 글자 표면에 빛 띠가 위아래로 흐른다. buildSheenShadow 함수는 각도 값에 층 번호(SHEEN_LAYER_COUNT=7)를 더한 위상을 사인 함수에 넣어 층마다 밝기를 22%~80%로 다르게 겹치고, 각도가 도는 동안 밝은 띠가 층을 타고 흐른다.

밝기 값이 층마다 달라 광택이 도는 금속 글자
새 창에서 열기

06 · 밝기 값이 층마다 달라 광택이 도는 금속 글자 — 측정 누적면적 10.386% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SHEEN_LAYER_COUNTbuildSheenShadowrenderSheen
  // 각도 값 하나로 층마다 밝기를 사인 곡선을 따라 다르게 겹쳐, 위·아래가 서로 다르게 빛나는
  // 금속 광택을 만든다. 층 위상(phase)이 갈릴수록 밝은 띠가 층을 타고 흐른다
  function buildSheenShadow(angleDeg) {
    var rad = (angleDeg * Math.PI) / 180;
    var parts = [];
    for (var i = 1; i <= SHEEN_LAYER_COUNT; i++) {
      var phase = rad + (i / SHEEN_LAYER_COUNT) * Math.PI * 1.4;
      var bright = (Math.sin(phase) + 1) / 2;
      var l = Math.round(22 + bright * 58);
      var y = (i * 6.8).toFixed(2);
      parts.push('0px ' + y + 'px 0 hsl(220 10% ' + l + '%)');
    }
    return parts.join(', ');
  }

06 항목은 누적면적 10.4%·강도 평균 43.9·23프레임 중 23프레임에서 움직였다.

07층 값마다 종이 한 장씩 겹친 듯한 글자

글자 아래로 크림색 그림자가 겹쳐 종이를 여러 장 쌓아 놓은 듯 보인다. buildPaperShadow 함수는 층 값(n)만큼 짝수·홀수 층 색을 다르게 OFFSET_STEP_PX(12px)씩 쌓고 옅은 그림자 한 겹을 더해, 1~6 사이를 오가는 n 값이 종이 장 수를 정한다.

층 값마다 종이 한 장씩 겹친 듯한 글자
새 창에서 열기

07 · 층 값마다 종이 한 장씩 겹친 듯한 글자 — 측정 누적면적 11.642% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PAPER_LAYER_COUNTbuildPaperShadowrenderPaper
  // 층 값(n) 하나로 색·거리 값이 다른 종이 층을 n 장 쌓는다 — n 이 늘수록 종이가
  // 여러 장 겹친 것처럼 보인다
  function buildPaperShadow(n) {
    var parts = [];
    for (var i = 1; i <= n; i++) {
      var d = (i * OFFSET_STEP_PX).toFixed(2);
      var warm = i % 2 === 0;
      var sheet = warm ? 'rgba(255,247,230,.92)' : 'rgba(246,238,222,.92)';
      parts.push(d + 'px ' + d + 'px 0 ' + sheet);
      parts.push(d + 'px ' + (Number(d) + 1.4).toFixed(2) + 'px 2px rgba(23,20,26,' + (0.22 - i * 0.02).toFixed(2) + ')');
    }
    return parts.join(', ');
  }

07 항목은 누적면적 11.6%·강도 평균 145.0·23프레임 중 18프레임에서 움직였다.

08스크롤 값만큼 그림자가 자라나는 글자

페이지를 내리는 동안 글자 아래 그림자가 점점 길고 흐릿하게 자라난다. buildScrollShadow 함수는 스크롤 진행 값(ratio 0~1)을 SCROLL_MAX_PX(26px)에 곱해 그림자 거리와 번짐을 함께 키우고, 조작 전까지는 진행 값을 자동으로 오르내리며 시연한다.

스크롤 값만큼 그림자가 자라나는 글자
새 창에서 열기

08 · 스크롤 값만큼 그림자가 자라나는 글자 — 측정 누적면적 9.894% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SCROLL_MAX_PXbuildScrollShadowrenderScroll
  // 스크롤 진행 값(ratio 0~1) 하나를 그림자 거리 값에 그대로 이어, 내려갈수록 그림자가 자란다
  function buildScrollShadow(ratio) {
    var d = (ratio * SCROLL_MAX_PX).toFixed(2);
    return [
      (d * 0.4).toFixed(2) + 'px ' + d + 'px 0 rgba(10,79,232,.5)',
      (d * 0.4).toFixed(2) + 'px ' + (d * 2).toFixed(2) + 'px ' + (4 + ratio * 10).toFixed(1) + 'px rgba(10,79,232,' + (0.28 + ratio * 0.3).toFixed(2) + ')'
    ].join(', ');
  }

08 항목은 누적면적 9.9%·강도 평균 66.5·23프레임 중 23프레임에서 움직였다.

09커서가 오르면 깊이 값이 밀려 올라오는 글자

포인터가 글자 위에 오르면 글자가 배경에서 떠오르듯 움직이고 그림자가 길어진다. buildLiftShadow 함수는 근접도 값(p 0~1)을 HOVER_LIFT_PX(4px)에 곱해 오프셋·번짐을 함께 밀어 올리고, curP 값이 LERP(0.18) 비율로 목표를 부드럽게 뒤따른다.

커서가 오르면 깊이 값이 밀려 올라오는 글자
새 창에서 열기

09 · 커서가 오르면 깊이 값이 밀려 올라오는 글자 — 측정 누적면적 11.546% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HOVER_LIFT_PXbuildLiftShadowrenderLift
  // 근접도 값(p 0~1) 하나로 그림자 오프셋·번짐 값을 함께 밀어 올려, 글자가 배경에서
  // 떠오르는 것처럼 보인다. 깊이 기본 단위는 HOVER_LIFT_PX
  function buildLiftShadow(p) {
    var y = (p * HOVER_LIFT_PX * 1.6).toFixed(2);
    var y2 = (p * HOVER_LIFT_PX * LIFT_MULTIPLIER).toFixed(2);
    var blur = (4 + p * HOVER_LIFT_PX * 6.5).toFixed(1);
    return [
      '0px ' + y + 'px 0 rgba(10,79,232,' + (0.55 + p * 0.3).toFixed(2) + ')',
      '0px ' + y2 + 'px ' + blur + 'px rgba(10,79,232,' + (0.22 + p * 0.36).toFixed(2) + ')'
    ].join(', ');
  }

09 항목은 누적면적 11.5%·강도 평균 48.3·23프레임 중 23프레임에서 움직였다.

css 텍스트 애니메이션 효과를 실제로 쓰는 화면 — 히어로 타이틀부터 카드 배지까지

01 돌출 입체는 랜딩 페이지 히어로 타이틀에, 02 엠보싱은 카드 배지 라벨에 붙는다. 03 윤곽선은 채움 없이도 눈에 띄어야 하는 이벤트 포스터 숫자에, 04 레트로 그림자는 빈티지 캠페인 헤드라인에 쓴다.

05 글리치는 오류 화면 헤드라인처럼 일부러 불안정해 보여야 하는 자리에, 06 금속 광택은 프리미엄 브랜드 로고 타입에 맞다. 07 종이 층은 카드뉴스 타이틀 그래픽에, 08 스크롤 그림자는 긴 소개 페이지의 섹션 제목에, 09 호버 부상은 포트폴리오 프로젝트 카드 제목에 자리 잡는다.

값이 그림자 모양을 바꾸는 곳 — 층 개수·오프셋 거리·색 분리

값이 꽂히는 자리는 세 갈래다. 01·06·07은 층 개수(SHADOW_LAYER_COUNT·SHEEN_LAYER_COUNT·PAPER_LAYER_COUNT)를 늘리거나 줄여 겹치는 그림자 장 수를 바꾸고, 02·03·04·08·09는 오프셋 거리(EMBOSS_DEPTH_PX·OUTLINE_MAX_PX·RETRO_MAX_LEN·SCROLL_MAX_PX·HOVER_LIFT_PX)를 늘려 그림자가 뻗는 길이를 바꾼다. 05만 색 분리 거리(GLITCH_SPLIT_PX)를 다뤄 두 색 그림자가 벌어지는 폭을 바꾼다.

NN 이름 누적면적 % 강도 평균 x/23
01 돌출 입체 10.4 26.0 22/23
02 엠보싱 10.9 129.2 23/23
03 윤곽선 10.6 140.2 21/23
04 레트로 그림자 10.1 140.3 22/23
05 글리치 분리 10.2 144.0 23/23
06 금속 광택 10.4 43.9 23/23
07 종이 층 11.6 145.0 18/23
08 스크롤 그림자 9.9 66.5 23/23
09 호버 부상 11.5 48.3 23/23

어디서 깨지는가 — 동작 줄임 설정·저사양 기기·긴 텍스트 줄바꿈

데모 아홉 개는 전부 white-space: nowrap 으로 한 줄을 강제해 둔다. 실제 페이지에서 이 규칙을 빼고 글자가 두 줄로 꺾이면, 대각선으로 쌓은 01·04·07의 층 그림자는 줄마다 따로 계산되지 않고 같은 오프셋을 그대로 반복해 두 줄 사이에서 층이 어긋나 보인다 — 한 줄짜리 제목·배지에만 붙이는 게 안전하다. zip 을 열어 코드를 그대로 붙였을 때 압축 비밀번호 kdbb8nch 로 폴더를 풀면 이 nowrap 줄도 그대로 들어 있어 원본 레이아웃과 다른 곳에 옮겨 붙일 때 무엇을 지웠는지 확인할 수 있다.

층이 많은 항목일수록 저사양 기기에서 부담이 커진다. 03은 여덟 방향, 07은 층마다 그림자 두 겹씩 최대 열두 겹을 매 프레임 style.textShadow 문자열로 다시 만든다 — text-shadow 는 transform 과 달리 합성(compositing)이 아니라 매번 다시 칠해야(paint) 해서, 층 수가 늘수록 저사양 기기의 프레임이 먼저 떨어진다. prefers-reduced-motion: reduce 를 확인하지 않고 requestAnimationFrame 루프를 그대로 돌리면 이 비용이 계속 쌓인다.

값 세 가지만 바꿔보는 변형과 접근성

변형 바꾼 값 느낌
입체 층 개수 값 조정 01 SHADOW_LAYER_COUNT 6 → 14 층이 절반이면 얇은 카드처럼 납작해지고, 14층이면 겹이 빽빽해져 블록 글자에 가까워진다
글리치 색 분리 거리 값 조정 05 GLITCH_SPLIT_PX 2 → 6 2px 는 화질이 나쁜 방송처럼 살짝만 어긋나고, 6px 는 색이 완전히 갈라져 판독이 어려운 수준까지 깨진다
호버 깊이 상승 값 조정 09 HOVER_LIFT_PX 4 → 12 4px 는 커서를 올려도 눈치채기 힘들고, 12px 는 글자가 카드 위로 튀어 오른 듯 그림자가 크게 벌어진다

아홉 항목 모두 window.matchMedia('(prefers-reduced-motion: reduce)').matches 값을 시작할 때 한 번 읽어, 참이면 루프를 아예 돌리지 않고 각 renderX 함수를 값 하나로 한 번만 호출해 둔다 — 01·03은 최대값(돌출·윤곽 최대치)에, 09는 근접도 0에 멈춘다. 글자 자체의 색($subject-ink·$subject-cream)은 애니메이션 대상이 아니라 textShadow 만 바뀌므로, 동작을 줄여도 배경 위 글자 대비는 움직이는 상태와 똑같이 유지된다.

css 텍스트 그림자 효과는 text-shadow 기본값과 뭐가 다른가?

text-shadow: 2px 2px 4px #000 한 줄만 쓰면 오프셋·번짐·색이 고정돼, 페이지를 새로고침해도 그림자는 늘 같은 모양이다. 이 아홉 개는 그 세 값을 JS 함수가 매 프레임 다시 계산해 넣는다는 점이 다르다 — 정적인 장식이 아니라 시간·스크롤·포인터 위치 같은 입력 하나로 그림자 자체가 움직이는 계산 결과다. 네온처럼 색을 발광시키는 CSS 글로우 효과 9종이 주로 filter: drop-shadow 확산에 집중한다면, 이 편은 그림자를 겹쳐 입체·질감을 만든다는 점에서 갈라지고, 밑줄·배경으로 강조하는 CSS 텍스트 강조 애니메이션 9종이나 등장 타이밍이 핵심인 CSS 텍스트 애니메이션 9종과도 다루는 속성 자체가 다르다. text-shadow 문법 자체의 기본값과 다중 그림자 표기법은 MDN text-shadow 문서에 정리돼 있다.

층을 몇 겹까지 쌓아도 되나 — 자주 묻는 것 셋

text-shadow 는 몇 겹까지 쓸 수 있나요?

문법상 개수 제한은 없다 — 쉼표로 이어 붙인 만큼 그림자가 쌓인다. 다만 07처럼 열두 겹을 매 프레임 새 문자열로 만들면 저사양 기기에서 다시 칠하는 비용이 커지므로, 정적인 장식이라면 값을 고정하고 JS 로 계산하는 건 애니메이션이 필요한 항목에만 남기는 편이 낫다.

텍스트 올라 오는 효과 css 도 이 방식으로 만들 수 있나요?

09 호버 부상이 그 방식이다 — transform: translateY 로 글자를 살짝 밀어 올리면서 같은 근접도 값(p)으로 그림자 오프셋과 번짐을 함께 키워, 이동과 그림자가 한 값에서 나오게 맞췄다. transform 만 쓰면 그림자가 따로 놀아 뜨는 느낌이 약해진다.

CSS 만으로 색 분리(글리치)가 되나요, JS 가 꼭 필요한가요?

text-shadow 두 겹을 고정값으로 넣으면 CSS 만으로도 되지만, 05처럼 매 클릭마다 다른 시드로 어긋나는 빈도까지 바꾸려면 계산이 필요하다. seedRand 는 Math.random 대신 정해진 규칙으로 다음 값을 만들어, 같은 시드에서는 항상 같은 결과가 나오게 해 둔 것뿐이다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.