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

css corner-shape 9종 — border-radius 없이 깎는 모서리 모양

아홉 가지 모서리 모양 중 하나는 슈퍼타원 지수 값이 원과 사각형 사이를 흐르며 카드 능선이 바뀌고, 하나는 스쿠어클 비율 값에 따라 모서리가 보간되고, 하나는 베벨 각도 값이 커질수록 빗면 넓이 값이 함께 커집니다.

아홉 데모는 각자 다른 조작으로 값을 받습니다 — 01·02는 click 하나, 04·05·07·08은 click과 keydown(Enter·Space·방향키), 03·09는 pointerdown·pointerup·pointerleave(03은 pointercancel까지)·keydown·keyup으로 누르고 떼는 동작을 직접 재고, 06만 pointerenter·pointerleave·focus·blur·click으로 상태를 바꿉니다. 아홉 항목 모두 자동 시연이 .is-demo 클래스가 붙어 있고 prefers-reduced-motion이 거짓일 때만 requestAnimationFrame 루프 안에서 돌다가, 사람이 처음 조작하는 순간 해제됩니다. 아홉 항목 모두 그 환경에서 CSS.supports('corner-shape', 'superellipse(1)') 호출로 지원 여부를 직접 재서, 참이면 corner-shape 속성을, 거짓이면 border-radius 근사 폴백을 씁니다.

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

아홉 개의 해부 — 어떤 값이 모서리 곡선을 바꾸는가

다음 표에는 css 요소가 화면을 얼마나 채웠는지·움직임이 얼마나 강했는지·몇 프레임이나 값이 갱신됐는지 세 값이 항목별로 들어 있습니다.

항목 누적면적% 강도_평균 움직인프레임
01 1.36% 109.8 23/23
02 0.33% 73.2 23/23
03 1.99% 90.0 21/23
04 3.21% 94.7 8/23
05 1.01% 109.7 23/23
06 3.25% 107.9 13/23
07 0.80% 107.2 15/23
08 1.42% 82.0 23/23
09 0.43% 82.8 23/23

아홉 항목 중 누적면적이 가장 넓은 항목은 06 하나(round 48px 원호와 bevel 6px 사이를 지수감쇠 lerp로 오가며 네 모서리 픽셀이 한꺼번에 바뀝니다)이고, 가장 좁은 항목은 02 하나(칩 한쪽 모서리의 빗면만 좁게 움직입니다)이며, 강도가 가장 센 항목도 01 하나(카드 능선 전체가 스무더스텝으로 편 흐름을 따라 끊김 없이 바뀝니다)뿐입니다. 강도가 가장 낮은 항목은 02(73.2 — 좁은 빗면 안에서 천천히 값이 바뀝니다)이고, 움직인프레임이 가장 많은 다섯 항목은 01·02·05·08·09(23/23 — 자동 시연이 쉬는 구간 없이 매 프레임 값을 다시 계산합니다)이며 가장 적은 항목은 04(8/23 — 탭이 한 칸 넘어간 뒤 스프링이 멈춰 있는 시간이 훨씬 깁니다)입니다.

이 값을 미는 계산 방식으로 아홉 항목을 다시 묶으면 네 무리가 나옵니다.

01·08 — 경과 시간 하나를 주기 함수에 바로 넣어 값을 구하는 것

01은 SWEEP_MS로 나눈 경과 시간의 왕복 삼각파(triangle)를 5차 다항식 스무더스텝(smootherstep)으로 편 값으로 슈퍼타원 지수를 MIN_N(1)에서 SUPER_N(4.0)까지 흘려보냅니다. 08은 (1-cos(t×2π))/2 코사인 왕복 값으로 반지름을 R_MIN(6)과 R_MAX(34) 사이에서 움직이고, 왼쪽 지원 칸에만 corner-shape: superellipse(SUPER_N)을 함께 얹어 오른쪽 폴백 칸과 나란히 겨룹니다. 둘 다 스프링처럼 목표를 뒤쫓지 않고, 그 순간의 경과 시간 하나가 값을 바로 정합니다.

03·07 — 3차 이징 곡선으로 구간 사이 값을 굽히는 것

03은 누르고 있는 시간 비율에 3차 ease-out(1-(1-t)³)을 걸어 스쿠프 반경을 SCOOP_MIN(4)에서 SCOOP_MAX(28)까지 굽혀 키우고, 뗄 때는 3차 ease-in(t³)으로 되돌립니다. 07은 다섯 단 고정 반경 목록(RUNGS)을 한 칸씩 옮길 때마다 3차 ease-in-out으로 STEP_MS(380ms) 동안 보간해, 값이 단 사이에서 뚝 끊기지 않습니다.

04·06·09 — 목표를 향해 감쇠로 붙거나 넘쳤다 되돌아오는 값

04는 감쇠 진동 공식 2^(-10t)·sin((10t-0.75)×2π/3)+1을 노치 깊이에 곱해, 탭이 바뀔 때 NOTCH_MAX(14)를 살짝 넘겼다 가라앉는 오버슛을 만듭니다. 06은 매 프레임 follow = 1-exp(-LERP_K×dt)를 구해 보간 비율을 목표에 붙이고, 비율이 절반을 넘으면 곡선 종류를 round에서 bevel로 바꿉니다. 09는 속곡선 반경을 겉 반경(OUTER_R 40)에서 인셋을 뺀 값으로 구하고, 누르고 있을 때와 뗄 때 목표가 바뀌는 복귀를 같은 지수감쇠 lerp(INSET_SPRING_K)로 되돌립니다.

02·05 — 각도·위상을 삼각비로 바로 계산하는 것

02는 베벨 각도의 탄젠트로 가로 깊이(rx = BEVEL_RY×tan(각도))를 구하고, 두 반경의 hypot 합산으로 빗면 넓이를 잽니다. 05는 네 모서리 각각에 π/2씩 어긋난 위상을 주고 r = BASE_R + AMP×sin(t×2π + phase)로 반경을 구해, 물결이 모서리를 한 바퀴 도는 순서를 만듭니다.

패턴별 코드

01스쿼클 카드 — 슈퍼타원 지수 값이 원과 사각형 사이를 흐르며 카드 모서리 능선이 바뀌는 방식

슈퍼타원 지수 값이 커질수록 모서리가 사각형에 가까워지고 1에 가까워질수록 직선 절단에 가까워져, 한 카드에서 곡률의 성질이 흐릅니다. 흐름은 SWEEP_MS(2400ms) 동안 삼각파를 스무더스텝으로 편 값을 따라 MIN_N(1)에서 SUPER_N(4.0)까지 오갑니다.

  // 계산 겹: 스무더스텝(5차 다항식, Perlin) 으로 왕복 삼각파를 매끄러운 흐름으로 편다 — 선형 트윈이 아니다
  function smootherstep(u) { return u * u * u * (u * (u * 6 - 15) + 10); }
  function triangle(x) { var f = x - Math.floor(x); return f < 0.5 ? f * 2 : (1 - f) * 2; }

  function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }

  function setN(n) {
    if (SUPPORTS) {
      card.style.cornerShape = 'superellipse(' + n.toFixed(3) + ')';
      card.style.borderRadius = '40px';
    } else {
      // 근사 폴백: 슈퍼타원 지수가 클수록 반경을 좁혀 각진 인상을 흉내(정확한 곡선은 아니다)
      card.style.borderRadius = (40 / n).toFixed(2) + 'px';
    }
    readout.textContent = '지수 값 ' + n.toFixed(2);
  }
스쿼클 카드 — 슈퍼타원 지수 값이 원과 사각형 사이를 흐르며 카드 모서리 능선이 바뀌는 방식
새 창에서 열기

01 · 스쿼클 카드 — 슈퍼타원 지수 값이 원과 사각형 사이를 흐르며 카드 모서리 능선이 바뀌는 방식 — 측정 누적면적 1.36% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SUPER_NSWEEP_MS$subject-blue

02베벨 칩 — 베벨 각도 값이 커질수록 칩 모서리 빗면의 넓이 값이 함께 커지는 방식

베벨 각도 값이 8도에서 18도까지 오르는 동안 빗면 넓이 값이 각도의 탄젠트를 따라 넓어져, 절단면이 얇은 선에서 면으로 자라납니다. 가로 깊이는 BEVEL_RY(14)×tan(각도)로 구하고, 빗면 넓이는 두 반경의 hypot 합산으로 잽니다.

  // 계산 겹: 베벨 각도 값에서 가로축 반경을 삼각비로 역산하고, 두 반경의 빗변 길이를 빗면 넓이로 삼는다
  function applyAngle(deg) {
    var rad = deg * Math.PI / 180;
    var rx = BEVEL_RY * Math.tan(rad);
    var facetWidth = Math.hypot(rx, BEVEL_RY);
    if (SUPPORTS) {
      chip.style.borderTopRightRadius = rx.toFixed(2) + 'px ' + BEVEL_RY + 'px';
    } else {
      // 근사 폴백: 대각 빗면 대신 같은 반경 크기의 둥근 모서리로 흉내
      chip.style.borderTopRightRadius = facetWidth.toFixed(2) + 'px';
    }
    readout.textContent = '빗면 넓이 ' + facetWidth.toFixed(1);
  }
베벨 칩 — 베벨 각도 값이 커질수록 칩 모서리 빗면의 넓이 값이 함께 커지는 방식
새 창에서 열기

02 · 베벨 칩 — 베벨 각도 값이 커질수록 칩 모서리 빗면의 넓이 값이 함께 커지는 방식 — 측정 누적면적 0.326% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BEVEL_DEGBEVEL_RY$subject-blue-deep

03스쿠프 버튼 — 모서리가 안쪽으로 파이는 scoop 곡선 값이 움직이며 버튼 윤곽이 바뀌는 방식

안쪽으로 파인 곡선 값이 깊어질수록 버튼 가장자리가 오목해져, 볼록한 둥근 버튼과 정반대 인상을 만듭니다. 누르고 있는 시간 비율에 3차 ease-out을 걸어 SCOOP_MIN(4)에서 SCOOP_MAX(28)까지 굽혀 키우고, HOLD_MS(700ms) 동안 유지한 뒤 ease-in으로 되돌립니다.

  // 계산 겹: 누르는 시간 비율을 3차 ease-out 으로 굽혀 스쿠프 반경에 매핑한다 — 선형 증가가 아니다
  function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
  function easeInCubic(t) { return t * t * t; }

  function applyScoop(r) {
    if (SUPPORTS) {
      btn.style.cornerShape = 'scoop';
      btn.style.borderRadius = r.toFixed(2) + 'px';
    } else {
      // 근사 폴백: 오목한 스쿠프 대신 같은 반경의 둥근 모서리로 흉내(오목/볼록만 다르다)
      btn.style.borderRadius = r.toFixed(2) + 'px';
    }
    readout.textContent = '스쿠프 반경 ' + r.toFixed(1);
  }
스쿠프 버튼 — 모서리가 안쪽으로 파이는 scoop 곡선 값이 움직이며 버튼 윤곽이 바뀌는 방식
새 창에서 열기

03 · 스쿠프 버튼 — 모서리가 안쪽으로 파이는 scoop 곡선 값이 움직이며 버튼 윤곽이 바뀌는 방식 — 측정 누적면적 1.99% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SCOOP_MAXHOLD_MS$subject-mint

04노치 탭 — 선택된 탭 모서리에 노치 깊이 값만큼 홈이 파이는 방식

활성 탭의 모서리에 노치 깊이 값이 지정한 만큼 홈이 새겨져, 어떤 탭이 열려 있는지 윤곽만으로 읽힙니다. 탭이 바뀐 순간부터 경과 비율을 감쇠 진동 공식에 넣어 NOTCH_MAX(14)를 살짝 넘겼다 가라앉는 스프링 오버슛(SPRING_MS 560ms)을 만듭니다.

  // 계산 겹: 탭이 바뀔 때 노치 깊이가 목표값을 살짝 넘겼다가 가라앉는 감쇠 진동(스프링 오버슛)으로 움직인다
  function springOvershoot(t) {
    if (t <= 0) return 0;
    if (t >= 1) return 1;
    var c4 = (2 * Math.PI) / 3;
    return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
  }

  function applyDepth(tab, depth) {
    if (SUPPORTS) {
      tab.style.cornerShape = 'notch';
      tab.style.borderRadius = Math.max(0, depth).toFixed(2) + 'px';
    } else {
      // 근사 폴백: 파인 노치 대신 같은 크기의 둥근 모서리로 흉내(파임/둥긂만 다르다)
      tab.style.borderRadius = Math.max(0, depth).toFixed(2) + 'px';
    }
  }
노치 탭 — 선택된 탭 모서리에 노치 깊이 값만큼 홈이 파이는 방식
새 창에서 열기

04 · 노치 탭 — 선택된 탭 모서리에 노치 깊이 값만큼 홈이 파이는 방식 — 측정 누적면적 3.212% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

NOTCH_MAXSPRING_MS$subject-sky

05물결 패널 — 네 모서리의 곡선 값이 위상차를 두고 순서대로 흔들리는 방식

네 모서리가 각각 다른 위상 값으로 곡선을 바꿔 물결이 한 바퀴 도는 것처럼 패널 윤곽이 살아 움직입니다. 모서리마다 π/2씩 어긋난 위상으로 r = BASE_R(20) + AMP(16)×sin(t×WAVE_MS 주기 − 위상)을 구합니다.

  // 계산 겹: 모서리마다 위상을 PI/2 씩 어긋낸 사인파 4개를 동시에 계산해 물결처럼 번갈아 부풀린다
  function corners(t) {
    var out = [];
    for (var i = 0; i < 4; i++) {
      var phase = i * (Math.PI / 2);
      var r = BASE_R + AMP * Math.sin(t * Math.PI * 2 + phase);
      out.push(Math.max(2, r));
    }
    return out;
  }

  function applyCorners(rs) {
    for (var i = 0; i < 4; i++) {
      if (SUPPORTS) panel.style.cornerShape = 'superellipse(2.2)';
      panel.style['border' + CORNERS[i] + 'Radius'] = rs[i].toFixed(2) + 'px';
    }
    readout.textContent = '위상 어긋남 ' + (rs[0] - rs[2]).toFixed(1);
  }
물결 패널 — 네 모서리의 곡선 값이 위상차를 두고 순서대로 흔들리는 방식
새 창에서 열기

05 · 물결 패널 — 네 모서리의 곡선 값이 위상차를 두고 순서대로 흔들리는 방식 — 측정 누적면적 1.01% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

AMPWAVE_MS$subject-lilac

06호버 모핑 — 대기 상태와 호버 상태의 모서리 곡선 값이 보간되며 넘어가는 방식

호버가 바뀔 때마다 두 곡선 값 사이를 보간 값이 매끈히 이어, 둥근 모양에서 베벨 모양으로 끊김 없이 넘어갑니다. 매 프레임 follow = 1-exp(-LERP_K(9)×dt)를 구해 목표에 붙이고, 비율이 절반을 넘으면 곡선 종류를 round(ROUND_R 48)에서 bevel(BEVEL_R 6)로 바꿉니다.

  // 계산 겹: 비율 값을 목표로 순간 이동시키지 않고 지수 감쇠(dt 기반 관성 lerp)로 붙인다 — 두 모양 사이 보간 비율이다
  var last = 0;
  function stepRatio(ts) {
    var dt = last ? (ts - last) / 1000 : 0;
    last = ts;
    var follow = 1 - Math.exp(-LERP_K * dt);
    ratio += (target - ratio) * follow;
    if (Math.abs(ratio - target) < 0.001) ratio = target;
  }

  function applyMorph() {
    var r = ROUND_R + (BEVEL_R - ROUND_R) * ratio;
    var shape = ratio < 0.5 ? (SUPPORTS_ROUND ? 'round' : null) : (SUPPORTS_BEVEL ? 'bevel' : null);
    if (shape) card.style.cornerShape = shape;
    card.style.borderRadius = r.toFixed(2) + 'px';
    readout.textContent = '보간 비율 ' + Math.round(ratio * 100) + '%';
  }
호버 모핑 — 대기 상태와 호버 상태의 모서리 곡선 값이 보간되며 넘어가는 방식
새 창에서 열기

06 · 호버 모핑 — 대기 상태와 호버 상태의 모서리 곡선 값이 보간되며 넘어가는 방식 — 측정 누적면적 3.251% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LERP_KROUND_R$subject-blue-deep

07반경 사다리 — 한 카드의 반경 값을 고정된 다섯 단 사다리에서 오르내리며 같은 곡선의 결을 보는 방식

다섯 단의 반경 사다리를 한 카드가 오르내리며, 단이 바뀔 때 값이 매끈히 보간되어 같은 곡선의 결이 크기마다 흐릅니다. 고정 반경 목록(RUNGS 4·12·20·28·36)을 STEP_MS(380ms) 동안 3차 ease-in-out으로 보간합니다.

  // 계산 겹: 사다리 칸 사이를 3차 ease-in-out 으로 보간한다 — 칸이 바뀌어도 값이 뚝 끊기지 않는다
  function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }

  function goTo(nextIdx, ts) {
    nextIdx = Math.max(0, Math.min(RUNGS.length - 1, nextIdx));
    if (nextIdx === idx) return;
    fromR = currentR(ts);
    idx = nextIdx;
    toR = RUNGS[idx];
    stepStart = ts;
    rungEl.textContent = String(idx + 1);
  }

  function currentR(ts) {
    var t = STEP_MS > 0 ? Math.min(1, (ts - stepStart) / STEP_MS) : 1;
    return fromR + (toR - fromR) * easeInOutCubic(t);
  }
반경 사다리 — 한 카드의 반경 값을 고정된 다섯 단 사다리에서 오르내리며 같은 곡선의 결을 보는 방식
새 창에서 열기

07 · 반경 사다리 — 한 카드의 반경 값을 고정된 다섯 단 사다리에서 오르내리며 같은 곡선의 결을 보는 방식 — 측정 누적면적 0.797% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RUNGSSTEP_MS$subject-pink

08폴백 나란히 보기 — 같은 반경 값을 받은 지원 칸과 폴백 칸의 윤곽을 나란히 비교하는 방식

같은 반경 값을 받은 지원 칸과 폴백 칸을 나란히 두고, 어느 모서리까지 똑같고 어디서부터 갈라지는지 보여줍니다. 반경은 (1-cos(t×2π))/2 코사인 왕복 값으로 R_MIN(6)과 R_MAX(34) 사이를 오가고, 왼쪽 칸만 corner-shape: superellipse(SUPER_N 2.6)을 함께 씁니다.

  // 계산 겹: 반경을 코사인으로 왕복시킨다 — 사인이 아니라 위상을 맞춘 코사인으로 0초에 하한에서 시작한다
  function radiusAt(t) {
    var c = (1 - Math.cos(t * Math.PI * 2)) / 2;
    return R_MIN + (R_MAX - R_MIN) * c;
  }

  function applyBoth(r) {
    if (SUPPORTS) {
      nativeBox.style.cornerShape = 'superellipse(' + SUPER_N.toFixed(1) + ')';
      nativeBox.style.borderRadius = r.toFixed(2) + 'px';
    } else {
      nativeBox.style.borderRadius = r.toFixed(2) + 'px';
    }
    // 오른쪽 칸은 언제나 border-radius 근사만 쓴다 — 두 렌더링의 차이를 나란히 겨룬다
    shimBox.style.borderRadius = r.toFixed(2) + 'px';
    readout.textContent = '지금 반경 ' + r.toFixed(1);
  }
폴백 나란히 보기 — 같은 반경 값을 받은 지원 칸과 폴백 칸의 윤곽을 나란히 비교하는 방식
새 창에서 열기

08 · 폴백 나란히 보기 — 같은 반경 값을 받은 지원 칸과 폴백 칸의 윤곽을 나란히 비교하는 방식 — 측정 누적면적 1.415% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

R_MAXSWEEP_MS$subject-lilac

09안쪽 테두리 — 겉곡선과 속곡선 값이 따로 놓여 이중 테두리가 생기는 방식

겉요소와 속요소가 서로 다른 곡선 값을 쓰면 두 윤곽 사이 간격이 곡선을 따라 달라져, 흐르는 이중 테두리가 만들어집니다. 속곡선 반경은 OUTER_R(40)에서 인셋을 뺀 산술로 구하고, 복귀는 INSET_SPRING_K(8) 지수감쇠로 INSET_MAX(22)까지 오갑니다.

  // 계산 겹: 인셋 산술 — 안쪽 반경 = 바깥 반경 − 인셋. 인셋이 커질수록 안쪽 곡선이 더 깎인다
  function innerRadius(inset) { return Math.max(2, OUTER_R - inset); }

  function applyInset(inset) {
    var r = innerRadius(inset);
    if (SUPPORTS) {
      frame.style.cornerShape = 'superellipse(3)';
      inner.style.cornerShape = 'superellipse(3)';
    }
    frame.style.borderRadius = OUTER_R + 'px';
    inner.style.borderRadius = r.toFixed(2) + 'px';
    readout.textContent = '인셋 ' + inset.toFixed(1);
  }
안쪽 테두리 — 겉곡선과 속곡선 값이 따로 놓여 이중 테두리가 생기는 방식
새 창에서 열기

09 · 안쪽 테두리 — 겉곡선과 속곡선 값이 따로 놓여 이중 테두리가 생기는 방식 — 측정 누적면적 0.428% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

OUTER_RINSET_MAX$subject-mint

값이 능선과 빗면을 바꾸는 곳 — 지수 값·각도 값·비율 값 기준

지수·각도·비율 어느 값이 모서리 곡선을 잡아당기는지 기준으로 아홉 항목을 다시 겹쳐 보면 세 무리가 나옵니다. 지수 값이 기준인 쪽은 01 하나로, corner-shape 명세의 superellipse 지수를 MIN_N에서 SUPER_N까지 흘려보내는 값이 능선의 결을 직접 정합니다. 각도 값이 기준인 쪽도 02 하나로, 베벨 각도의 탄젠트가 빗면 넓이를 정합니다. 비율 값이 기준인 쪽은 여섯이나 됩니다 — 03(스쿠프 반경 비율)·04(노치 깊이 오버슛 비율)·06(모핑 보간 비율)·07(사다리 단 사이 보간 비율)·08(코사인 왕복 비율)·09(인셋 스프링 비율)처럼, 0에서 1 사이 진행값 하나가 그 프레임의 반경이나 깊이를 정합니다.

05는 이 세 갈래에 들지 않고 위상 값 하나를 씁니다 — 네 모서리 각각에 걸린 위상차가 사인 함수 안에서 반경을 정해, 지수·각도·비율 어느 쪽으로도 묶이지 않습니다.

이 편의 아홉 항목은 corner-shape 하나로 모서리 곡선의 지수·각도·비율 값을 계산하는 것이 축이라, 호버 시 그림자와 이동 값을 계산하는 CSS 카드 애니메이션 호버 효과 9종 — 한 줄 복붙으로와는 다루는 값 자체가 다릅니다. 뱃지 디자인 UI 9종 — 신상·세일·인증 배지 코드는 색과 형태로 상태를 구분하는 값을 다루는 반면, 이 편은 모서리 곡선 하나의 계산만 다룹니다. 사진 없이 윤곽만으로 사용자를 표시하는 값 계산은 아바타 UI 9종 — 사진 파일 없이 만드는 사용자 표시에서 다루고, 이 편의 09(안쪽 테두리)는 그 원형 윤곽과 달리 두 겹의 corner-shape 곡선 값 차이로 이중 테두리를 만듭니다.

어디서 깨지는가 — 미지원 브라우저, 폴백, reduced-motion

아홉 항목 모두 각 환경에서 CSS.supports('corner-shape', 'superellipse(1)') 호출로 지원 여부를 직접 재서, 참이면 corner-shape 속성(superellipse·bevel·scoop·notch·squircle·round)을 쓰고 거짓이면 border-radius 근사 폴백으로 같은 자리를 채웁니다. 08은 이 감지를 그대로 시연해 지원 칸과 폴백 칸을 나란히 두고, 어느 모서리까지 똑같고 어디서부터 갈라지는지 보여줍니다. 근사 폴백은 정확한 곡선이 아니라 반경 하나로 흉내 낸 것이라, 슈퍼타원처럼 지수가 큰 모양일수록 지원 환경과 폴백 환경의 모서리가 눈에 띄게 달라집니다.

reduced-motion이 켜지면 아홉 항목이 저마다 다른 값 하나로 멈춥니다 — 01은 지수를 SUPER_N으로 즉시 고정하고, 02는 각도를 BEVEL_DEG로 고정하며, 03은 스쿠프 반경이 즉시 결정되고, 04는 노치 깊이가 스냅되며, 05는 위상을 고정값으로 멈추고, 06은 target=1로 비율이 즉시 정해지며, 07은 사다리 최상단 칸에 고정되고, 08은 t=0.5 지점에 고정되며, 09는 INSET_MAX로 고정됩니다. 전 항목이 같은 방식으로 멈춘다고 단정할 수 없는 이유가 여기 있습니다 — 계산 한 겹마다 멈추는 값의 자리가 다릅니다.

값을 직접 눌러보며 상수를 하나씩 바꿔보고 싶다면, x4zdvxz7 압축 비밀번호로 아홉 항목의 html·scss·js 파일 전체를 받아 코드를 그대로 열어볼 수 있습니다.

저사양 기기에서 아홉 데모를 한 화면에 전부 재생하면 requestAnimationFrame 루프 아홉 개가 동시에 돌아 프레임이 밀릴 수 있으므로, 실제로 붙일 때는 한 화면에 필요한 항목 하나만 남기는 편이 낫습니다. css corner shape generator 같은 도구 없이도, 아홉 상수 값을 zip 안에서 직접 바꿔보면 같은 모서리 곡선을 손으로 만들 수 있습니다. 이 중 css corner shape bevel 방식에 해당하는 것은 02 베벨 칩 하나로, 각도 값의 탄젠트가 빗면 넓이를 직접 정합니다.

변형 세 가지와 접근성

변형 값 느낌
스쿼클 지수 올리기 01 SUPER_N 4.0 → 8.0 슈퍼타원 지수 상한을 4.0에서 8.0으로 올리면 흐름이 닿는 능선이 한층 사각형 쪽으로 깊어진다. 소스는 지수를 MIN_N(1)에서 SUPER_N까지 매끄러운 보간으로 흘려보내므로, 상한 하나만 바꿔도 전체 흐름의 결이 달라진다.
베벨 각도 키우기 02 BEVEL_DEG 18 → 36 베벨 각도 상한을 18도에서 36도로 올리면 빗면 넓이가 탄젠트를 따라 선형보다 크게 벌어져 절단면이 도드라진다. 소스는 가로 깊이를 BEVEL_RY × tan(각도)로 계산하고 빗면 넓이를 hypot로 재므로, 각도를 두 배로 하면 빗면은 두 배보다 넓어진다.
지수 순환 빠르게 하기 01 SWEEP_MS 2400 → 1200 지수 순환 주기를 2400ms에서 1200ms로 줄이면 원과 사각형 사이를 오가는 흐름이 정확히 두 배 빨라져 변화가 또렷해진다. 소스는 경과 시간을 SWEEP_MS로 나눈 삼각파로 지수를 흘려보내므로, 주기를 절반으로 줄이는 것이 곧 두 배 속도다.

아홉 항목 모두 상태 문구를 aria-live="polite" readout에 담아 스크린 리더가 지금 값을 읽게 하고, 01·02·03·05·06·09는 버튼에 aria-pressed를 상태에 맞춰 true·false로 바꿉니다. 04는 role="tab"·aria-selected로 탭 세 개의 열림 상태를 알리고, 07은 aria-label에 방향키 조작법을 함께 적어 둡니다. 키보드는 04·05·07·08이 Enter·Space·방향키로, 03·09는 스페이스를 누르고 있는 동안 값이 바뀌도록 keydown·keyup으로 직접 조작되며, reduced-motion에서는 앞서 본 것처럼 항목마다 다른 값 하나로 즉시 고정되어 애니메이션 없이도 최종 모양은 그대로 보입니다.

FAQ

css squircle 카드에 border-radius만 써도 되나요?

border-radius만 쓰면 네 모서리가 전부 같은 곡률로 둥글게 깎여, 슈퍼타원 지수가 흘러가며 능선의 결이 바뀌는 01의 움직임은 재현할 수 없습니다. corner-shape가 지원되는 환경에서는 superellipse(지수) 값을 직접 넣어야 사각형에 가까운 결과까지 갈 수 있고, 지원되지 않는 환경에서는 반경만으로 흉내 낸 폴백이 됩니다.

이 아홉 패턴을 실제 서비스 버튼·카드에 그대로 붙여도 되나요?

네, 아홉 항목 모두 외부 라이브러리 없이 css·js 값 계산만으로 동작해 zip 안 코드를 그대로 붙이면 바로 움직입니다. 다만 corner-shape 미지원 환경에서는 border-radius 근사 폴백만 보이므로, 지수가 큰 슈퍼타원처럼 폴백과 차이가 큰 모양은 두 환경을 나란히 한 번 확인해 보는 편이 안전합니다.

아홉 항목 중 계산이 가장 무거운 건 어떤 항목인가요?

06은 매 프레임 지수감쇠 lerp를 반경과 곡선 종류 모두에 적용하고 누적면적도 3.25%로 가장 넓어, 화면에서 픽셀이 바뀌는 범위가 아홉 중 가장 큽니다. 02는 반대로 빗면 하나만 좁게 움직여 누적면적이 0.33%로 가장 좁고 강도평균도 73.2로 가장 낮아, 계산량은 비슷해도 화면에 미치는 영향은 훨씬 작습니다.

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

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