GODRICH

종이 질감 디자인 랜딩 키트, 오려 붙인 섹션 9개

종이 질감 디자인은 색종이를 가위로 오려 붙인 것처럼 면마다 흐림 없는 그림자로 두께를 주고, 색은 다섯 가지 안에서만 쓰는 스타일입니다. 종이 사진을 깔지 않고 그림자 값 하나만으로 종이처럼 보이게 만드는 것이 전부입니다.

핀터레스트에서 "paper cutout"으로 저장되는 화면들은 대부분 포토샵으로 만든 이미지라 웹으로 옮기면 무거워집니다. 여기서는 이미지를 한 장도 쓰지 않고 CSS 값만으로 같은 인상을 냈습니다. 아래 원페이지를 스크롤하면서 종이처럼 보이는 이유가 어디에 있는지 먼저 찾아보세요.

새 창에서 원페이지 열기

저 페이지를 이루는 부품 아홉 개를 하나씩 떼어내 아래 격자에 늘어놓았습니다. 순서는 인기순이 아니라 종이 한 장이 화면에서 겪는 차례를 따랐습니다. 오려서 올리고, 자리를 옮기고, 여러 장을 겹치고, 스티커로 찍고, 모서리를 접고, 테이프로 붙이고, 찢고, 흘려보내고, 마지막으로 눌립니다.

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

01오려 붙인 종이 히어로

흰 종이 한 장이 떠오르는 동안 뒤에 깔린 색종이 세 조각은 반대로 내려앉습니다. 두 방향이 엇갈려야 겹친 층이 층으로 보입니다. 그림자는 종이가 올라간 만큼 넓고 옅어집니다.

겹친 층반대 위상radius 16
.heropaper__sheet {
  box-shadow: 0 10px 18px rgba(42, 35, 32, .14);
  animation: sheetLift 2s ease-out infinite;
}
.heropaper__scrap { animation: scrapDrift 2s ease-out infinite; }
.heropaper__scrap--butter { animation-delay: -.66s; }
.heropaper__scrap--leaf   { animation-delay: -1.33s; }

@keyframes sheetLift {
  0%, 100% { transform: translateY(0);      box-shadow: 0 10px 18px rgba(42, 35, 32, .14); }
  50%      { transform: translateY(-12px);  box-shadow: 0 22px 28px rgba(42, 35, 32, .1); }
}
@keyframes scrapDrift {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(8px) rotate(3deg); }
}

02들리는 종이 탭 내비

밑줄 대신 노란 종이 조각이 탭 뒤에서 한 칸씩 뛰어 옮겨 갑니다. 칸을 옮길 때마다 5px 들렸다 앉기 때문에 미끄러지는 게 아니라 집어서 옮기는 것처럼 보입니다.

고정 폭 탭왕복 이동radius 12
.navpaper__tabs { --step: 76px; }        // 탭 폭 72 + 간격 4
.navpaper__tab  { flex: 0 0 72px; }      // 고정 폭이라 이동량과 렌더 폭이 어긋나지 않는다
.navpaper__chip { animation: chipHop 2s cubic-bezier(.2, .8, .2, 1) infinite; }

@keyframes chipHop {
  0%   { transform: translateX(0) translateY(0); }
  12%  { transform: translateX(0) translateY(-5px); }
  25%  { transform: translateX(var(--step)) translateY(0); }
  50%  { transform: translateX(calc(var(--step) * 2)) translateY(0); }
  75%  { transform: translateX(var(--step)) translateY(0); }
  100% { transform: translateX(0) translateY(0); }
}

03색종이 머리띠 카드 격자

흰 카드 세 장의 위쪽 가장자리에 색종이 띠를 하나씩 얹었습니다. 카드 본체는 전부 흰색이고 색은 띠와 아이콘에만 들어갑니다. 세 장이 0.22초씩 어긋나 차례로 들립니다.

시차 0.22s머리띠 색종이radius 16
.cardpaper { display: flex; gap: 12px; align-items: stretch; }
.cardpaper__card {
  box-shadow: 0 6px 0 rgba(42, 35, 32, .16);   // 흐림 0 = 종이 두께
  animation: cardPeel 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
.cardpaper__card::before {                      // 위에 얹은 색종이 띠
  content: ""; position: absolute; left: 12px; right: 12px; top: -8px;
  height: 16px; border-radius: 4px;
}
.cardpaper__card--sky  { animation-delay: -.22s; }
.cardpaper__card--leaf { animation-delay: -.44s; }

@keyframes cardPeel {
  0%, 100% { transform: translateY(0);     box-shadow: 0 6px 0 rgba(42, 35, 32, .16); }
  45%      { transform: translateY(-14px) rotate(-2deg); box-shadow: 0 18px 8px rgba(42, 35, 32, .12); }
}

04스티커 체크 기능 목록

동그란 스티커 네 장이 순서대로 찍힙니다. 0.45배까지 눌렸다가 1.18배로 튀어 오른 뒤 제자리로 돌아오는 세 단계가 도장 찍는 손맛을 만듭니다. 스티커 색은 네 장이 하나씩 나눠 씁니다.

scale 0.45→1.18네 색 스티커radius 50%
.stickerlist__dot {
  box-shadow: 0 3px 0 rgba(42, 35, 32, .2);
  animation: stickerStamp 2s cubic-bezier(.2, 1.4, .4, 1) infinite;
}
.stickerlist__row:nth-child(2) .stickerlist__dot { animation-delay: -1.75s; }
.stickerlist__row:nth-child(3) .stickerlist__dot { animation-delay: -1.5s; }
.stickerlist__row:nth-child(4) .stickerlist__dot { animation-delay: -1.25s; }

@keyframes stickerStamp {
  0%, 60%, 100% { transform: scale(1) rotate(0deg); }
  10%           { transform: scale(.45) rotate(-12deg); }
  22%           { transform: scale(1.18) rotate(6deg); }
  34%           { transform: scale(1) rotate(0deg); }
}

05모서리 접힌 요금제

추천 플랜의 오른쪽 위 모서리가 종이 뒷면 색으로 접혔다 펴집니다. 삼각형은 clip-path 로 잘라 두고, 접히는 동작은 축을 모서리 꼭짓점에 둔 scale 하나로 끝냅니다.

clip-path 삼각형transform-origin 100% 0radius 16
.dogear__plan { position: relative; overflow: hidden; }
.dogear__fold {
  position: absolute; top: 0; right: 0; width: 40px; height: 40px;
  background: #f6c453;                          // 종이 뒷면 색
  clip-path: polygon(100% 0, 0 0, 100% 100%);
  transform-origin: 100% 0;                     // 꼭짓점을 축으로 접는다
  animation: cornerFold 2s ease-out infinite;
}
@keyframes cornerFold {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(.28); }
}

06테이프로 붙인 후기

메모지 위쪽 두 귀퉁이를 마스킹 테이프로 눌러 붙였습니다. 회전축을 종이 바깥 12px 위에 두면 같은 각도라도 아래쪽이 훨씬 크게 흔들려, 테이프에 매달린 무게가 느껴집니다.

transform-origin 50% -12px테이프 두 장radius 8
.tapenote {
  transform-origin: 50% -12px;                  // 축이 종이 밖 — 아래쪽이 크게 움직인다
  animation: noteSway 2s cubic-bezier(.2, .8, .2, 1) infinite;
}
.tapenote__tape   { position: absolute; top: -10px; width: 56px; height: 20px; opacity: .92; }
.tapenote__tape--l { left: 16px;  transform: rotate(-6deg); }
.tapenote__tape--r { right: 16px; transform: rotate(5deg); }

@keyframes noteSway {
  0%, 100% { transform: rotate(-1.6deg); }
  50%      { transform: rotate(1.6deg); }
}

07찢은 가장자리 CTA 밴드

위아래를 지그재그로 찢은 띠입니다. 톱니는 이미지가 아니라 마스크 세 겹으로, 위아래를 깎아 내는 conic-gradient 둘에 본체를 남기는 linear-gradient 하나입니다. 움직이는 것은 버튼 하나뿐이고, 버튼이 내려앉는 만큼 그림자 두께도 줄어듭니다.

conic-gradient 마스크그림자=두께radius 12
.tornband {
  mask:
    conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 14px 7px repeat-x,
    conic-gradient(from 135deg at top,    #0000, #000 1deg 89deg, #0000 90deg) top    / 14px 7px repeat-x,
    linear-gradient(#000 0 0) center / 100% calc(100% - 14px) no-repeat;
}
.tornband__btn { animation: paperPress 2s cubic-bezier(.2, .8, .2, 1) infinite; }

@keyframes paperPress {
  0%, 100% { transform: translateY(-8px); box-shadow: 0 14px 0 rgba(42, 35, 32, .3); }
  50%      { transform: translateY(0);    box-shadow: 0 2px 0 rgba(42, 35, 32, .3); }
}

08색종이 띠 푸터

맨 아래 띠에 네 색 조각을 두 벌 깔고 한 벌만큼만 밉니다. 한 바퀴가 끝난 순간이 첫 프레임과 픽셀 단위로 같아지므로 이음매가 없습니다. 간격을 gap 으로 주면 이 계산이 깨집니다.

두 벌 복제margin-right 간격translateX(-50%)
.pfoot__track {
  display: flex;
  width: max-content;
  animation: stripFlow 2s linear infinite;
}
.pfoot__bit {
  width: 36px; height: 20px;
  margin-right: 12px;        // gap 은 2N 조각 사이에 2N-1 개뿐이라 -50% 가 어긋난다
  border-radius: 4px;
}
@keyframes stripFlow {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

09종이 버튼·인풋

버튼 두 개가 반 주기씩 어긋나 6px 내려앉습니다. 그림자 6px 이 종이 두께였으니, 눌리면 두께가 사라지는 것이 물리적으로 맞습니다. 입력칸은 반대로 안쪽 그림자를 써서 파낸 홈이 됩니다.

그림자=두께inset 홈radius 12
.paperui__field {
  box-shadow: inset 0 3px 0 #e7dccb;            // 파낸 홈 — 바깥 그림자를 같이 주지 않는다
}
.paperui__btn--solid { box-shadow: 0 6px 0 rgba(42, 35, 32, .28); }
.paperui__btn--ghost {
  box-shadow: 0 6px 0 #e7dccb, inset 0 0 0 2px #e7dccb;
  animation-delay: -1s;                          // 반 주기 뒤 — 둘이 번갈아 눌린다
}
@keyframes btnPress {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}

tailwind.config 커스텀

이 스타일에서 값을 한 곳에 모아야 하는 이유는 색이 아니라 그림자입니다. 바닥에 붙은 종이의 두께(shadow-paper)와 들어올렸을 때의 흐린 그림자(shadow-lift)를 섞어 쓰면 종이로 안 보이기 때문에, 둘을 이름부터 갈라 둡니다.

theme: {
  extend: {
    colors: {
      paper: "#fbf6ec", sheet: "#ffffff", ink: "#2a2320", stone: "#5c5048",
      coral: "#cc3d26", sky: "#2f6fb8", leaf: "#2b7d5a",
      butter: "#f6c453", blush: "#f6b3ab", edge: "#e7dccb",
    },
    borderRadius: { card: "16px", control: "12px", chip: "4px", pill: "999px" },
    boxShadow: {
      paper: "0 6px 0 rgba(42,35,32,.16)",      // 바닥에 붙은 종이 = 두께
      "paper-edge": "0 6px 0 #e7dccb",
      lift: "0 18px 8px rgba(42,35,32,.12)",    // 들어올렸을 때만
      "lift-high": "0 22px 28px rgba(42,35,32,.1)",
      groove: "inset 0 3px 0 #e7dccb",          // 파낸 홈
    },
  },
},
역할 토큰 언제
종이 두께 shadow-paper 카드·버튼이 바닥에 붙어 있을 때
들어올림 shadow-lift 호버·활성 상태에서만
파낸 홈 shadow-groove 입력칸 안쪽

바이브코딩 프롬프트

설정을 넣어 뒀다면 Claude Code나 Cursor에 넘길 지시는 아래 한 덩어리면 충분합니다. 색 목록보다 그림자 규칙이 먼저 오는 순서가 중요합니다. 줄이지 않은 원문은 zip 안의 prompt.md 에 있습니다.

바닥에 붙은 종이의 그림자는 흐림을 0 으로 준다: 0 6px 0 rgba(42,35,32,.16). 이 6px 이
종이 두께이고 화면 전체에서 같은 값이다. 들어올릴 때만 0 18px 8px 로 흐린 그림자로
바꾼다. 배경은 paper(#fbf6ec) 한 톤, 면은 sheet(#ffffff) 한 톤. 색종이는 다섯 가지고
한 화면에 셋까지만 쓴다. 흰 글자는 coral·sky·leaf 위에만 얹는다. 찢은 가장자리는
이미지가 아니라 conic-gradient 마스크로 만든다.

이 지시대로 page/index.html 을 만든 뒤 1280px 뷰포트에서 전체 페이지를 찍은 것이 아래 그림입니다.

결과

어디서 깨지는가 — 함정

가장 오래 못 찾은 것은 글자 크기였습니다. 부품 아홉 개의 스타일에 font: 800 24px/1.1; 처럼 한 줄로 크기와 굵기를 몰아 쓰고 바로 아랫줄에 font-family: inherit; 을 뒀는데, 렌더된 화면에서 요금 숫자가 제목보다 작아 보였습니다. Playwright로 계산값(computed style)을 직접 찍어 보니 그 숫자는 24px이 아니라 16px/400, 히어로 제목은 의도한 22px이 아니라 브라우저 기본값인 32px/700 이었습니다. font 단축 속성은 마지막 자리에 font-family 가 반드시 있어야 하고, 비어 있으면 선언 전체가 버려집니다. 아홉 개 파일에 퍼진 21곳을 font-weight·font-size·line-height 낱개 속성으로 풀고 나서야 잰 값과 쓴 값이 같아졌습니다.

두 번째는 원페이지 쪽이었습니다. Tailwind 플레이 CDN을 쓰면서 평범한 <style> 안에 body { background: theme('colors.paper') } 를 적었는데, 화면은 그냥 흰색이었고 요금제 카드의 접힌 모서리는 아예 보이지 않았습니다. 계산된 배경색을 찍으니 둘 다 rgba(0, 0, 0, 0) — 플레이 CDN은 type="text/tailwindcss" 가 붙은 style 만 컴파일하고, 그 밖의 <style> 안에 있는 theme() 는 무효로 버립니다. 이 두 가지를 다 손본 완성본은 압축 비밀번호 rysqfgwr 으로 열리는 zip 안에 그대로 들어 있습니다.

접근성

밝은 종이 톤은 대비를 계산하기 쉬운 편이지만, 색종이 위에 흰 글자를 얹는 순간 기준선에 걸립니다. WCAG 2.1 최소 대비 기준의 상대 휘도 공식으로 직접 계산해 보면 잉크(#2a2320)는 종이 바탕(#fbf6ec)에서 14.34:1, 보조 글자 스톤(#5c5048)은 7.23:1로 본문 기준 4.5:1을 넉넉히 넘습니다. 문제는 강조색이었습니다. 처음 고른 산호색 #d6452f는 흰 글자와 4.42:1로 기준선 아래였고, #cc3d26까지 어둡게 내려서야 4.93:1이 나왔습니다. 같은 방식으로 하늘색(#2f6fb8) 5.14:1, 잎색(#2b7d5a) 5.02:1을 확보했고, 버터(#f6c453)와 블러시(#f6b3ab)는 너무 밝아 흰 글자로는 기준을 맞출 수 없어서, 잉크 글자만 얹도록(각각 9.52:1, 8.79:1) 코드에서 제한했습니다.

움직임 쪽은 MDN prefers-reduced-motion 문서의 미디어 쿼리를 아홉 개 전부에 넣어, 동작 줄이기를 켠 방문자에게는 종이가 뜨거나 띠가 흐르는 움직임 없이 정지 상태만 보이게 했습니다. 이때도 MDN box-shadow 문서대로 그린 두께 그림자는 그대로 남으므로 층 구조는 읽힙니다. 찢은 가장자리에 쓴 MDN mask 문서의 마스크는 장식이라 지원하지 않는 브라우저에서는 네모난 띠로 보일 뿐 내용은 그대로입니다.

다른 홈페이지 유행이 궁금하면 디자인 트렌드 카테고리를 둘러보시고, 이 블로그가 뭘 주는 곳인지는 소개 페이지에 적어 뒀습니다.

FAQ

종이 질감 이미지를 깔면 더 그럴듯하지 않나요?

질감 사진을 깔면 파일이 무거워지고 배경색을 바꿀 때마다 이미지를 다시 만들어야 합니다. 이 키트가 종이처럼 보이는 이유는 질감이 아니라 흐림이 0 인 그림자와 한 방향으로 통일된 두께입니다. 종이 사진 없이도 층이 보이면 그때부터 질감은 선택 사항이 됩니다.

그림자 두께를 요소마다 다르게 줘도 되나요?

한 화면에서는 같은 값을 쓰는 편이 안전합니다. 두께가 제각각이면 보는 사람은 그것을 "종이 두께 차이"가 아니라 "정렬 실수"로 읽습니다. 두께를 바꾸고 싶다면 요소마다 다른 값을 흩뿌리지 말고 6px에서 3px 또는 10px로 화면 전체를 한 번에 옮기세요.

어두운 배경에서도 이 스타일이 되나요?

두께 그림자가 배경보다 어두워야 층이 보이는데, 어두운 배경에서는 그 여지가 없어집니다. 굳이 쓴다면 그림자를 어둡게 하는 대신 각 종이 위쪽에 밝은 테두리 한 줄(inset 0 1px 0)을 얹어 빛을 받는 면을 만드는 쪽이 낫습니다. 다만 그 순간 이 스타일의 밝고 가벼운 인상은 사라집니다.

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

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