Container Scroll <mv-container-scroll>

유리에 반사광이 비치며 뒤로 기울어진 기기 화면이, 스크롤되어 보이는 동안 똑바로 서고 자리를 잡으며 반사광을 걷어 내고, 그사이 제목이 떠올라 제자리에 놓입니다.

작동 방식

유리에 반사광이 비치며 뒤로 기울어진 기기 화면이, 스크롤되어 보이는 동안 똑바로 서고 자리를 잡으며 반사광을 걷어 내고, 그사이 제목이 떠올라 제자리에 놓입니다. 키프레임 세트 하나를 사용하며, 스크롤 중에는 스크립트 없이 네이티브 view timeline으로 실행되거나 가벼운 JS 폴백이 위치를 탐색합니다.

카테고리스크롤
유형Web Component (<mv-container-scroll>)
상태안정
Keywordssafe-rewrite, 3d, perspective, device, mockup, hero, product, view-timeline, scroll-driven

When to use

  • A landing page needs a product screenshot or video that flattens into view as the visitor scrolls to it
  • A launch section pairs a headline with an app screen and should feel like the device is being presented

Avoid when

  • The device frame should stay flat and static, with no scroll animation → use Device Mockup instead
  • The screenshot carries essential detail users must read while it is still tilted and scaled

설치

node scripts/add.mjs container-scroll --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["container-scroll"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/element.js, core/observe.js, components/container-scroll/container-scroll.js, components/container-scroll/container-scroll.css.

사용법

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="mv-cscroll-demo" class="mv-scroll-area" data-orientation="vertical" tabindex="0" aria-label="Container scroll demo, scroll to present the screen">
  <style>
    #mv-cscroll-demo { width: min(100%, 56rem); height: 460px; margin-inline: auto; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: radial-gradient(70% 55% at 50% 75%, var(--mv-accent-subtle), transparent 70%), var(--mv-bg); }
    #mv-cscroll-demo .hint { height: 230px; display: grid; place-content: center; justify-items: center; gap: .5rem; color: var(--mv-fg-muted); font-size: .85rem; }
    #mv-cscroll-demo .hint i { width: 1.25rem; height: 2rem; border: 2px solid currentColor; border-radius: 99px; position: relative; }
    #mv-cscroll-demo .hint i::after { content: ""; position: absolute; left: 50%; top: .35rem; width: 3px; height: .45rem; margin-left: -1.5px; border-radius: 2px; background: currentColor; animation: mv-cscroll-demo-wheel 1.8s var(--mv-ease-in-out) infinite; }
    @keyframes mv-cscroll-demo-wheel { 60% { translate: 0 .5rem; opacity: 0; } }
    @media (prefers-reduced-motion: reduce) { #mv-cscroll-demo .hint i::after { animation: none; } }
    #mv-cscroll-demo mv-container-scroll { padding: 0 1.5rem 7rem; }
    #mv-cscroll-demo .mv-container-scroll-title small { font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--mv-accent-fg); font-weight: 600; }
    #mv-cscroll-demo .mv-container-scroll-title h2 { margin: .45rem 0 0; font: 700 clamp(1.6rem, 4vw, 2.6rem)/1.06 var(--mv-font-display); letter-spacing: -.035em; text-wrap: balance; }
    #mv-cscroll-demo .mv-container-scroll-title p { margin: .6rem auto 0; max-width: 30rem; color: var(--mv-fg-muted); font-size: .95rem; }
    #mv-cscroll-demo .mv-container-scroll-card { max-width: 44rem; height: 19rem; }

    #mv-cscroll-demo .board { display: grid !important; grid-template-rows: auto 1fr; background: var(--mv-bg); color: var(--mv-fg); font-size: .72rem; }
    #mv-cscroll-demo .bar { display: flex; align-items: center; gap: .6rem; padding: .7rem 1rem; border-bottom: 1px solid var(--mv-border); }
    #mv-cscroll-demo .bar b { font-size: .85rem; margin-right: auto; }
    #mv-cscroll-demo .bar span { padding: .2rem .55rem; border-radius: 99px; background: var(--mv-bg-muted); color: var(--mv-fg-muted); }
    #mv-cscroll-demo .bar span:last-child { background: var(--mv-accent); color: var(--mv-fg-on-accent); }
    #mv-cscroll-demo .cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: .7rem; padding: .8rem 1rem; min-height: 0; }
    #mv-cscroll-demo .col { display: grid; align-content: start; gap: .45rem; padding: .55rem; border-radius: .7rem; background: var(--mv-bg-subtle); }
    #mv-cscroll-demo .col > small { color: var(--mv-fg-muted); font-weight: 600; }
    #mv-cscroll-demo .task { display: grid; gap: .3rem; padding: .5rem .6rem; border-radius: .55rem; border: 1px solid var(--mv-border); background: var(--mv-surface); }
    #mv-cscroll-demo .task em { font-style: normal; color: var(--mv-fg-muted); }
    #mv-cscroll-demo .task u { text-decoration: none; justify-self: start; padding: .05rem .4rem; border-radius: 99px; font-size: .62rem; background: var(--mv-accent-subtle); color: var(--mv-accent-fg); }
  </style>

  <div class="hint"><i aria-hidden="true"></i>Scroll to present the screen</div>

  <mv-container-scroll>
    <div class="mv-container-scroll-title">
      <small>Roadmap</small>
      <h2>Ship the next release without the spreadsheet</h2>
      <p>Plan, assign and track every task in one shared board.</p>
    </div>
    <div class="mv-container-scroll-card">
      <div class="board" role="img" aria-label="Project board with three columns: To do, In progress and Done, each holding task cards">
        <div class="bar"><b>Release 4.2</b><span>Due Oct 14</span><span>8 of 13 done</span></div>
        <div class="cols">
          <div class="col"><small>To do</small>
            <div class="task">Localize checkout in Japanese<em>Kenji Watanabe</em><u>i18n</u></div>
            <div class="task">Audit color contrast<em>Amara Okafor</em><u>a11y</u></div>
          </div>
          <div class="col"><small>In progress</small>
            <div class="task">Faster image uploads<em>Lucas Moreau</em><u>perf</u></div>
            <div class="task">New pricing page<em>Priya Nair</em><u>web</u></div>
          </div>
          <div class="col"><small>Done</small>
            <div class="task">Apple Pay on mobile<em>Sofia Rossi</em><u>payments</u></div>
            <div class="task">Dark mode emails<em>Diego Alvarez</em><u>email</u></div>
          </div>
        </div>
      </div>
    </div>
  </mv-container-scroll>
</div>

API

Attributes

Name유형Description
scrollerselector | windowReference scroll container; setting it switches to the JS path. Default: the nearest scroll container, or the page.
fallbackbooleanForces the JavaScript path (tests, browsers without view timelines).

Content structure

NameDescription
.mv-container-scroll-titleTitle above the screen; rises and brightens while the screen straightens.
.mv-container-scroll-cardDevice frame (solid bezel, lit edge, camera dot, glare); its child is the screen (image, video, mockup).

CSS variables

NameDefaultDescription
--mv-container-scroll-rotate20degStarting lean (rotateX around the bottom edge).
--mv-container-scroll-scale1.05Starting scale of the screen.
--mv-container-scroll-lift1.75remStarting offset of the title (downward).
--mv-container-scroll-perspective900pxPerspective depth.
--mv-container-scroll-width60remMax. screen width.
--mv-container-scroll-gapvar(--mv-space-8)Space between the title and the screen.
--mv-container-scroll-glareoklch(1 0 0 / 0.22)Color of the glare resting on the glass while it leans back.
--mv-container-scroll-frame / -bezel / -radiusgray-800 / 0.625rem / 1.75remFrame color, bezel thickness, outer radius.

Accessibility

Purely visual: give the screen alternative text (image alt or role="img" + aria-label). No scroll hijacking: the native path runs on a view timeline with no script; the fallback uses one passive listener and one frame at a time, only while the section is near its scrollport. The motion goes from the moment the section has entered its scrollport to the moment it is centered. Reduced motion (OS setting or data-motion="reduce"): the resting layout at once, flat screen and title in place. Without JS or support: the resting layout. Forced colors: a system outline replaces the frame shading, no glare.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고