본문으로 건너뛰기
wooncloud

background-clip: content-box 는 border-radius 도 깎는다

background-clip을 content-box로 설정하면 padding 두께만큼 border-radius가 깎여 의도치 않게 각진 모양이 되는데, 이를 해결하려면 슬래시 문법을 사용해 가로와 세로 반경을 개별적으로 보정해야 합니다

·6 min read· views·

background-clip 이 모서리 반경을 깎는 모습

on/off 스위치를 <button> 하나로 그렸다. 트랙은 button 자신의 배경이고, knob 은 position: absolute 인 자식 하나. 트랙을 자식으로 중첩하면 부모 flex 에서 눌려 knob 이 밖으로 삐져나오는 문제가 있어서 이 구조로 고정해 뒀다.

모바일 터치 타겟 44px 을 맞추려면 22px 짜리 트랙에 세로 여백이 필요하다. 그런데 padding 을 주면 배경도 같이 커진다. 그래서 box-sizing: content-box 로 padding 이 크기를 밀어내게 하고, background-clip: content-box 로 배경은 content box 안에만 그리게 했다. 히트 영역은 44px, 보이는 트랙은 50×22.

여기까지는 잘 됐는데, border-radius: 11px(높이의 절반)을 준 트랙이 pill 이 아니라 각진 사각형으로 렌더됐다.

.track {
  box-sizing: content-box;
  width: 50px;
  height: 22px;
  padding: 11px 0;            /* 44px 히트 영역 */
  border-radius: 11px;        /* pill 이어야 하는데... */
  background: #5b40f8;
  background-clip: content-box;
}

computed style 은 거짓말을 하지 않는다, 다만 답도 아니다

DevTools 에서 확인한 border-radius11px 그대로다. 지정한 값이 무시된 게 아니다. 문제는 배경을 자를 때 쓰는 반경이 지정값과 다르다는 것.

CSS Backgrounds & Borders Level 3 은 padding edge 와 content edge 의 모서리 반경을 이렇게 정의한다: 바깥 반경에서 그 변의 border/padding 두께를 뺀 값. 음수면 0.

여기 세로 padding 이 11px 이니까,

  • 가로 반경: 11 - 0 = 11 (좌우 padding 은 0)
  • 세로 반경: 11 - 11 = 0

세로 반경이 0 이면 곡선이 사라진다. 각진 사각형이 나온 이유다. background-clip: border-box(기본값)였다면 바깥 반경을 그대로 써서 멀쩡했을 텐데, content box 로 자르는 순간 이 계산이 끼어든다.

큰 값으로 밀어붙이면 이번엔 타원이 된다

반경이 깎이는 게 문제라면 넉넉하게 주면 되지 않나 — border-radius: 999px 로 바꿔봤다. 결과는 더 나빴다. 좌우가 과하게 부푼 타원이 되면서 knob 이 트랙 밖으로 삐져나왔다.

큰 값은 먼저 border box 기준으로 클램프된다. border box 는 50×44(padding 포함)이므로 반경이 22 로 잘리고, 거기서 padding 을 빼면

  • 가로 반경: 22 - 0 = 22
  • 세로 반경: 22 - 11 = 11

가로 22 / 세로 11 의 타원. 좌우 곡선이 트랙 안쪽 깊숙이 파고들기 때문에, 오른쪽 끝에 붙인 knob 의 위아래 모서리가 트랙 바깥으로 노출된다.

단일 값으로는 이 문제를 풀 수 없다. 가로와 서로 다른 양만큼 깎이는데 뺄 값을 하나만 줄 수 있기 때문이다.

슬래시 문법으로 가로·세로를 따로 준다

border-radius/ 로 가로 반경과 세로 반경을 분리해서 쓸 수 있다. 원하는 최종 반경 r 에 대해, 가로는 r, 세로는 r + 세로 padding 을 주면 깎인 뒤에 정확히 r 이 남는다.

.track {
  box-sizing: content-box;
  width: 50px;
  height: 22px;
  padding: 11px 0;
  border-radius: 11px / 22px; /* 가로 11 · 세로 11+11 → content box 에서 정확히 pill */
  background: #5b40f8;
  background-clip: content-box;
}

일반화하면 border-radius: r / calc(r + <padding-block>). 좌우에도 padding 이 있다면 가로 쪽에도 같은 보정이 붙는다.

테스트로 못박을 때는 값이 아니라 관계를

이런 버그는 값을 단언하는 테스트로는 못 잡는다. border-radius: 11px 도 통과하고 999px 도 통과한다. 둘 다 화면에서는 깨져 있는데.

깨지는 조건이 padding 과의 관계이므로, 단언도 관계로 써야 한다.

const [h, v] = el.style.borderRadius.split("/").map(parseFloat);
const padY = parseFloat(el.style.padding);   // "11px 0px" → 11
const height = parseFloat(el.style.height);
 
expect(h).toBe(height / 2);          // 좌우 padding 0 → 깎이지 않는다
expect(v - padY).toBe(height / 2);   // 상하 padding 만큼 미리 더해 둔다

그리고 이 계열의 버그는 결국 실제 브라우저에 띄워 눈으로 확인해야 확정된다. jsdom 은 배경 클리핑을 계산하지 않고, computed style 은 클립 후의 실효 반경을 알려주지 않는다. 후보 값 몇 개를 한 페이지에 나란히 놓고 스크린샷을 찍는 게 제일 빠르다.

정리

  • background-clippadding-boxcontent-box 로 바꾸면 모서리 반경도 그 변의 두께만큼 깎여서 적용된다.
  • 가로·세로 padding 이 다르면 단일 반경 값으로는 원하는 모양이 안 나온다. r / (r + padding) 처럼 분리해서 준다.
  • 히트 영역을 padding 으로 벌리는 패턴(스위치, 작은 아이콘 버튼, 칩)에서 특히 자주 만난다.

참고