안녕하세요, 디자인 엔지니어 올리브예요. 멋진 사이트 분석 4편이에요.

Nina Park의 첫 화면 시간차 등장과 서비스 목록 호버는 CSS만으로 따라 만들 수 있어요. 첫 화면은 요소마다 transition-delay를 0.1~0.2초씩 다르게 줘서 차례로 올라오게 하고, 목록은 grid-template-rows: 0fr → 1fr로 세부 항목을 펼쳐요. 아래 데모는 이 블로그 색으로 바꿔서 직접 만든 거예요. 다시 보기를 누르거나 마우스를 올려 보세요.

3편에서는 Nina Park의 흑백 화면, 소문자 제목, 글꼴 세 가지를 봤어요. 그중 제 눈에 가장 오래 남은 건 두 가지 움직임이었어요. 원본이 어떻게 움직이는지 프레임 단위로 재 보고, 같은 속도로 만들어 봤어요.

구분 원본에서 본 것 만드는 법
첫 화면 등장 배경 → 제목 → 태그 → 사진 → 소개 문장 순서로 하나씩 올라와요 transition-delay 시간차
서비스 목록 호버 올린 줄만 펼쳐지며 바탕·글자색이 바뀌고 세부 항목이 나와요 grid-template-rows 0fr → 1fr

1. 첫 화면은 어떤 순서로 나타나나요?

원본은 어떻게 움직이나요?

사이트에 처음 들어가거나 새로고침하면, 메뉴만 먼저 보이고 나머지는 정해진 순서대로 하나씩 나타나요. 배경이 시작한 때를 0초로 놓고 재 보니 이랬어요.

순서 요소 시작 길이 움직임
1 하프톤 배경 0초 약 0.9초 1.2배 → 1배로 줄어들며 나타나요
2 제목 첫 줄 "nina park" 0.7초 약 0.8초 10px 아래에서 올라오며 나타나요 (0.98배 → 1배)
3 제목 둘째 줄 "visual designer" 0.9초 약 0.8초 첫 줄과 같아요
4 태그 5개 0.9초부터 0.1초 간격 약 0.3초 하나씩 10px 올라와요
5 사진·예약 문구·버튼 묶음 1.1초 약 1초 80px 아래에서 크게 올라와요
6 소개 문장 세 줄 1.6초부터 0.1초 간격 약 0.8초 한 줄씩 10px 올라와요

모든 움직임이 처음에 빠르고 끝에서 천천히 멈추는 감속 곡선이에요. 재미있는 건 제목이 글자 단위로 쪼개져 있는데도, 한 줄 안의 글자들은 모두 같은 순간에 움직였다는 점이에요. 그래서 따라 만들 때는 글자를 쪼갤 필요 없이 줄 단위로 움직여도 똑같이 보여요.

따라 만들면?

"다시 보기"를 눌러 보세요. 순서와 시작 시간, 길이는 위 표에서 잰 값 그대로예요.

<style>
  .stage { position: relative; display: flex; flex-direction: column; height: 100vh; padding: 14px 18px 18px; overflow: hidden; background: #F4F6F4; }
  /* 배경: 원본의 하프톤 사진 대신 연초록 원과 줄무늬 */
  .bg {
    position: absolute; inset: 0;
    background:
      radial-gradient(circle at 78% 32%, #E5F2E7 0 20%, transparent 20.5%),
      radial-gradient(circle at 18% 88%, #E5F2E7 0 14%, transparent 14.5%),
      repeating-linear-gradient(-45deg, transparent 0 7px, rgba(0, 163, 15, .05) 7px 8px);
  }
  .stage > :not(.bg) { position: relative; }
  .nav { display: flex; justify-content: space-between; align-items: center; font-size: 11px; letter-spacing: .06em; color: #6B7F6E; }
  .replay { padding: 4px 10px; border: 0; border-radius: 3px; background: #0A1F0E; color: #fff; font: inherit; cursor: pointer; }
  .title { margin: 26px 0 0; font-size: 56px; font-weight: 400; line-height: 1.02; letter-spacing: -0.01em; }
  .row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; }
  .tags { display: flex; flex-wrap: wrap; gap: 4px; max-width: 190px; }
  .tag { padding: 2px 8px; border: 1px solid #0A1F0E; border-radius: 999px; font-size: 10px; letter-spacing: .04em; }
  .bottom { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin-top: auto; }
  .intro { margin: 0; font-size: 17px; line-height: 1.4; }
  .intro span { display: block; }
  .card { width: 168px; flex: none; font-size: 10px; letter-spacing: .04em; }
  .photo { height: 96px; margin: 4px 0; background: linear-gradient(135deg, #00A30F, #007A0B); }
  .cta { display: inline-block; padding: 5px 8px; border-radius: 3px; background: #0A1F0E; color: #fff; }

  /* 시간차 등장: 요소마다 --d(시작), --t(길이), --y(올라오는 거리)만 다르다 */
  .bg { opacity: 0; transform: scale(1.2); }
  .rise { display: inline-block; opacity: 0; transform: translateY(var(--y, 10px)) scale(var(--s, .98)); }
  .stage.in .bg {
    opacity: 1; transform: none;
    transition: opacity .9s cubic-bezier(.45, 0, .25, 1), transform .9s cubic-bezier(.45, 0, .25, 1);
  }
  .stage.in .rise {
    opacity: 1; transform: none;
    transition: opacity var(--t, .8s) cubic-bezier(.25, 1, .5, 1) var(--d), transform var(--t, .8s) cubic-bezier(.25, 1, .5, 1) var(--d);
  }
  .tag { --t: .3s; --s: 1; }
  .card { --t: 1s; --y: 80px; }

  @media (max-width: 420px) {
    .title { margin-top: 20px; font-size: 34px; }
    .tags { max-width: none; }
    .bottom { flex-direction: column; align-items: flex-start; gap: 12px; }
    .intro { font-size: 14px; }
    .card { width: 150px; }
    .photo { height: 64px; }
  }
</style>
<div class="stage">
  <div class="bg"></div>
  <div class="nav"><span>OLIVEVES</span><button type="button" class="replay">다시 보기</button></div>
  <h1 class="title">
    <span class="rise" style="--d: .7s; display: block;">olive</span>
    <span class="row">
      <span class="rise" style="--d: .9s;">design engineer</span>
      <span class="tags">
        <span class="tag rise" style="--d: .9s;">WEB DESIGN</span>
        <span class="tag rise" style="--d: 1s;">FRAMER</span>
        <span class="tag rise" style="--d: 1.1s;">SEO</span>
        <span class="tag rise" style="--d: 1.2s;">AI TOOLS</span>
        <span class="tag rise" style="--d: 1.3s;">FIGMA</span>
      </span>
    </span>
  </h1>
  <div class="bottom">
    <p class="intro">
      <span class="rise" style="--d: 1.6s;">웹디자인과 AI 도구를</span>
      <span class="rise" style="--d: 1.7s;">직접 써 보고,</span>
      <span class="rise" style="--d: 1.8s;">겪은 일을 기록해요.</span>
    </p>
    <div class="card rise" style="--d: 1.1s;">
      NEW POSTS EVERY WEEK
      <div class="photo"></div>
      <span class="cta">READ THE BLOG ↗</span>
    </div>
  </div>
</div>
<script>
  const stage = document.querySelector('.stage');
  function play() {
    stage.classList.remove('in');
    stage.offsetWidth; // 처음 상태(숨김)를 한 번 그리게 한다
    stage.classList.add('in');
  }
  // 화면에 들어오면 한 번 재생
  new IntersectionObserver(([e], io) => {
    if (e.isIntersecting) { play(); io.disconnect(); }
  }, { threshold: 0.4 }).observe(stage);
  document.querySelector('.replay').addEventListener('click', play);
</script>
  • 순서는 transition-delay 하나로: 요소마다 --d만 다르게 주고, 같은 클래스(.in)를 붙이는 순간 각자 정해진 시간에 출발해요.
  • 거리와 길이로 무게 주기: 태그는 10px·0.3초로 가볍게, 사진 묶음은 80px·1초로 묵직하게 올라와요. 같은 등장이라도 요소 크기에 맞춰 달라요.
  • 배경은 반대로 줄어들기: 다른 요소가 아래에서 올라오는 동안 배경만 1.2배에서 1배로 줄어들어서, 화면 전체가 '자리 잡는' 느낌이 나요.

2. 서비스 목록은 어떻게 펼쳐지나요?

원본은 어떻게 움직이나요?

마우스를 올리면 그 줄의 높이가 76px에서 260px로 0.2초 만에 커져요. 처음에 빨리 커지고 끝에서 천천히 멈추는 감속(ease-out) 곡선이에요. 같은 0.2초 동안 이런 일이 함께 일어나요.

  • 바탕: 흰색 → 아주 옅은 회색 #F1F1F1
  • 번호: 거의 안 보이는 #F1F1F1 → 회색 #939393
  • 서비스 이름: 회색 #777 → 검정 #111
  • 줄 안쪽 여백이 왼쪽 0 → 20px로 늘어서 글자가 오른쪽으로 살짝 밀려요
  • 아래에 8px 회색 네모 점이 붙은 세부 항목 4개(18px)가 나타나요

마우스를 떼면 같은 0.2초 동안 다시 접혀요. 다른 줄로 옮기면 앞 줄이 접히는 동안 새 줄이 펼쳐져서, 목록이 물결처럼 움직여요.

따라 만들면?

목록에 마우스를 올려 보세요. 모바일에서는 줄을 눌러 보세요. 속도와 곡선은 원본에서 잰 값(0.2초, 감속)에 맞췄어요.

<style>
  .stage { display: grid; justify-items: center; align-content: start; height: 100vh; padding: 28px 20px; background: #F4F6F4; }
  .list { width: min(520px, 100%); margin: 0; padding: 0; list-style: none; }
  /* 줄 하나 = 제목 줄 + 접혀 있는 세부 항목 (원본: 0.2초 감속) */
  .item {
    --ease: cubic-bezier(.5, 1, .89, 1);
    display: grid; grid-template-rows: auto 0fr;
    padding: 4px 20px 4px 0;
    border-bottom: 1px solid #E6E9E6; background: #fff;
    cursor: default; outline: none;
    transition: grid-template-rows .2s var(--ease), padding .2s var(--ease), background-color .2s var(--ease);
  }
  .item:hover, .item:focus { grid-template-rows: auto 1fr; padding: 14px 20px 26px 16px; background: #E5F2E7; }
  .title { display: flex; gap: 12px; font-size: 34px; line-height: 1.25; letter-spacing: -0.01em; }
  .num { width: 26px; color: #E6E9E6; transition: color .2s var(--ease); }
  .name { color: #6B7F6E; transition: color .2s var(--ease); }
  .item:hover .num, .item:focus .num { color: #00A30F; }
  .item:hover .name, .item:focus .name { color: #0A1F0E; }
  /* 0fr일 때 높이 0이 되도록 min-height: 0 + overflow: hidden */
  .sub { min-height: 0; overflow: hidden; }
  .sub ul { margin: 0; padding: 8px 0 0 38px; list-style: none; display: grid; gap: 6px; }
  .sub li { display: flex; align-items: center; gap: 10px; font-size: 15px; color: #6B7F6E; }
  .sub li::before { content: ""; width: 7px; height: 7px; background: #00A30F; }
  @media (max-width: 420px) {
    .title { font-size: 22px; gap: 8px; }
    .num { width: 18px; }
    .sub ul { padding-left: 26px; }
    .sub li { font-size: 13px; }
  }
</style>
<div class="stage">
  <ul class="list">
    <li class="item" tabindex="0"><div class="title"><span class="num">1</span><span class="name">brand strategy</span></div>
      <div class="sub"><ul><li>positioning</li><li>naming</li><li>brand story</li></ul></div></li>
    <li class="item" tabindex="0"><div class="title"><span class="num">2</span><span class="name">visual identity</span></div>
      <div class="sub"><ul><li>logo &amp; wordmark</li><li>color &amp; type system</li><li>guidelines</li></ul></div></li>
    <li class="item" tabindex="0"><div class="title"><span class="num">3</span><span class="name">web design</span></div>
      <div class="sub"><ul><li>landing page</li><li>portfolio site</li><li>design system</li></ul></div></li>
    <li class="item" tabindex="0"><div class="title"><span class="num">4</span><span class="name">framer development</span></div>
      <div class="sub"><ul><li>cms setup</li><li>interactions</li><li>launch</li></ul></div></li>
  </ul>
</div>
  • 높이 대신 grid-template-rows: height: auto는 전환이 안 되지만, 행 크기 0fr → 1fr은 부드럽게 바뀌어요. 세부 항목 칸에는 min-height: 0과 overflow: hidden을 꼭 같이 줘요.
  • 여백도 같이 전환: 왼쪽 여백 0 → 16px이 글자를 오른쪽으로 밀어서, 펼쳐지는 줄이 '앞으로 나오는' 느낌을 줘요.
  • cubic-bezier(.5, 1, .89, 1) 0.2초: 원본처럼 빨리 열리고 끝에서 살짝 늦춰져요. 키보드(:focus)와 모바일 터치에서도 펼쳐지게 tabindex="0"을 달았어요.

올리브의 한마디

첫 화면 등장은 처음 볼 때 "오, 예쁘다" 하고 지나가기 쉬운데, 재 보니 꽤 꼼꼼하게 짜여 있었어요. 제목은 0.2초 간격, 태그는 0.1초 간격, 사진은 조금 늦게 무겁게. 무엇을 먼저 읽게 할지를 시간으로 정해 둔 것이라, 등장 순서가 곧 읽는 순서가 돼요.

서비스 목록은 보기에는 단순한데, 높이·여백·바탕·글자색 다섯 가지가 똑같이 0.2초 안에 함께 움직이고 있었어요. 하나라도 박자가 어긋나면 덜컹거렸을 거예요. 짧고 빠른 전환이 오히려 '잘 만든 느낌'을 준다는 걸 다시 배웠어요. 이 블로그 첫 화면에도 시간차 등장을 하나쯤 가져와 볼까 해요.

오늘도 읽어 주셔서 고마워요. 다음 글에서 만나요! 🌿