MOTION PALETTE
← すべてのデモ

Counter Scroll

ScrollGSAP / ScrollTrigger

フルスクリーンで開く ↗

枠の中でスクロール・クリックして動きを試せます。端末指定は実寸表示です。幅が足りない場合は横にスクロールできます。

再現プロンプト

架空のファッションレーベル「OFFSET STUDIO」のルックブック1ページサイト。左右2カラムが互いに逆走するスクロール(右カラムは自身の高さの18%ぶんを±に走るtranslateYスクラブ)が核で、中央の縫い目進捗ラインと、カードが画面中央=縫い目と交差する瞬間のスケール強調で「シンメトリーのずれ」を体感させる。 配色・構成・動きの数値をまとめた指示書です。 お使いのAIに貼り、素材やお店の情報を伝えて実装してください。仕上がりは利用するAIと素材によって変わります。

Build a single self-contained HTML file (inline CSS/JS) that is the complete one-page website for "OFFSET STUDIO" — a fictional fashion label whose motto is "symmetry, interrupted". Use GSAP + ScrollTrigger via CDN (`https://unpkg.com/gsap@3/dist/gsap.min.js`, `https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js`) — no other external libraries or fetch calls. Supporting graphics (the hero's two panels) are procedurally generated with pure CSS (layered `repeating-linear-gradient` fabric textures and hard-stop `linear-gradient` colour blocks); the twelve lookbook cards, however, each show a photograph — supply twelve 3:4 portrait editorial fashion images of your own (e.g. `img/look-01.jpg` … `img/look-12.jpg` next to the HTML file) and reference them with plain `<img>` tags, giving every card a solid colourway `background-color` fallback so a missing image still degrades gracefully. The page scrolls normally with `window`/`document.documentElement` scroll (the internal-scroll-container constraint described below is specific to the MOTION PALETTE embed and not needed standalone — build one long scrolling page in document flow and drive every ScrollTrigger `scroller` off the default window scroller instead).

## Art direction

Cream background `#F6F1EA` throughout, ink text `#201B18`, muted copy `#8C8177`, hairline borders `rgba(32,26,22,0.14)`, accent orchid `#E879F9` reserved for the seam line/needle, hero seam divider, and hover/CTA accents. Headings use an elegant italic display serif (Cormorant-style, weights 300–700 with an italic cut), always set in italic at weight 500; UI/body copy uses a clean grotesque sans (Work Sans-style, 400–600). Eyebrow labels: 0.7rem, letter-spacing 0.28em, uppercase, muted, weight 600. 1px hairline dividers between sections, section padding `clamp(3rem,8vh,6rem) clamp(1.25rem,5vw,3rem)`. Never use a marquee, a magnetic button, a scroll-triggered count-up stats block, or a generic numbered "01/02/03" service list anywhere on this page — all entrances instead use the page's own alternating left/right offset-converge, described below.

## Signature entrance — "offset converge" (used everywhere instead of a generic fade-up)

Every eyebrow, lede, statement, craft note and footer line is wrapped in a reveal target and assigned an alternating direction in DOM order: even-indexed elements start at `x:-24px`, odd-indexed at `x:+24px`, both at `opacity:0`. Use `ScrollTrigger.batch(targets, {start:"top 85%", onEnter: batch => gsap.to(batch, {opacity:1, x:0, duration:0.6, ease:"power3.out", stagger:0.08}), onLeaveBack: batch => gsap.to(batch, {opacity:0, x: el => el.dataset.dir==="r" ? 24 : -24, duration:0.3, ease:"power2.in"})})`. Under `prefers-reduced-motion: reduce`, set every target to its resting `opacity:1, x:0` immediately and skip the batch entirely.

## Section 1: Hero

Full-viewport-height, two-column grid (copy left, visual right; visual moves to `order:-1` and stacks above copy below 780px). The visual is a bordered box (`height:min(64cqh,560px)`) split into two vertical panels by a 2px solid accent-orchid seam divider (opacity 0.6) — panel A: a diagonal colour-block composition (`linear-gradient(165deg, base 0% 42%, block 42% 76%, pattern 76% 100%)` in the taupe colourway); panel B: a herringbone fabric texture (two overlapping 45°/-45° `repeating-linear-gradient`s, 16px tile, offset 8px) in the charcoal colourway. Both panels idle-float independently and permanently, never resting: panel A `translateY(-8px→8px)`, 7s ease-in-out infinite alternate; panel B `translateY(10px→-10px)`, 8.5s ease-in-out infinite alternate — deliberately different periods and opposite phase so the two panels visibly drift apart and together, previewing the page's counter-scroll idea before any scrolling happens. Heading "Nothing lines up / on purpose." renders as two stacked italic serif lines that each animate once on mount (not scroll-triggered): line 1 slides in from `x:-30px`, line 2 from `x:+30px`, both to `x:0, opacity:1` over 0.8s `cubic-bezier(0.22,1,0.36,1)`, staggered 0.1s/0.24s delay. Below: a lede paragraph ("Two instincts, two speeds, one collection. OFFSET STUDIO designs every look in pairs and lets them drift — the tension between them is the point.") and a pill CTA "View the collection ↓" (1px ink border, hover: ink fill + cream text + `translateY(-2px)`) that smooth-scrolls to the Lookbook section — both using the offset-converge entrance.

## Section 2: Statement

Eyebrow "THE LABEL"; an italic serif line "We design in twos, and let them drift apart." (`clamp(1.8rem,4.6vw,3rem)`, max-width 18ch); a lede paragraph (muted, max-width 56ch): "OFFSET STUDIO builds every collection as a pair of instincts moving at different speeds — one considered, one impulsive. Nothing is symmetrical on purpose; the misalignment is the signature, not a flaw to correct."

## Section 3: The Lookbook — the counter-scrolling column pair (core signature)

Eyebrow "THE COLLECTION", heading "Twelve looks, running counter to each other.", lede explaining the mechanic. Below, a 3-column CSS grid `1fr 2px 1fr` (column gap `clamp(1rem,3vw,2rem)`, 100px vertical padding as scroll buffer) holding: a left column of 6 look cards in plain document flow (**no JavaScript transform is ever applied to it** — this is literally "left runs in the normal direction"); a 2px centre "seam" column; a right column whose 6 cards live inside one `.column-inner` wrapper that DOES receive an extra scroll-linked transform on top of normal flow.

**Counter-scroll mechanism, with exact ratio**: on mount (and on every resize), measure `shift = rightInner.getBoundingClientRect().height * 0.18` (ratio = 0.18, i.e. the right column's total counter-travel equals 18% of its own natural content height) and set its resting transform to `translateY(-shift)`. Create one `ScrollTrigger` on the Lookbook `<section>` (`start:"top bottom", end:"bottom top", scrub:0.6`); on every `onUpdate`, with `p = self.progress` (0→1 across the section's full scroll-through), set `rightInner.style.transform = translateY(-shift + p*shift*2)` — i.e. a straight linear sweep from `-shift` to `+shift` (total travel = `2*shift`, ≈36% of the column's own height) exactly in sync with scroll via scrub. Because the left column never moves beyond normal page scroll while the right column's cards additionally slide through this ±18% sweep, the two columns visibly run counter to one another — near the top of the section the right column reads "ahead", by the middle it realigns with the left, and by the bottom it reads "behind". Below 780px viewport width, or under `prefers-reduced-motion: reduce`, skip creating this ScrollTrigger entirely, reset the transform to `none`, and collapse the grid to a single column (seam hidden via `display:none`) — the two look lists simply stack top-to-bottom in DOM order.

## Signature — the seam progress line + the crossing-scale pulse

The seam column holds three stacked absolutely-positioned layers: a static dashed "stitch" track (`repeating-linear-gradient(to bottom, hairline 0 6px, transparent 6px 12px)`, 2px wide, full section height); a solid accent-orchid fill bar with `transform-origin:top`, driven by the *same* `onUpdate` callback as the counter-scroll (`seamFill.style.transform = scaleY(p)`) so it fills top-to-bottom exactly as the section scrolls through; and a 10px accent-orchid glowing "needle" dot (`box-shadow: 0 0 10px 3px rgba(232,121,249,0.55)`) whose `top` is set to `${p*100}%` every update, so it always marks the seam's current fill position. In the same `onUpdate`, for every look card (both columns — read positions via native `getBoundingClientRect()`, which already reflects the right column's live transform, so no extra math is needed there): compute `cardCenterY`, compare against the scroller viewport's vertical centre (`scrollerRect.top + scrollerRect.height/2` — the same height as the needle's travel line), derive `proximity = clamp(1 - dist/(cardHeight*0.9), 0, 1)`, then apply `transform: scale(1 + proximity*0.08)` and `filter: brightness(1 + proximity*0.06)` directly to the card. The effect: whichever card (in either column) is currently passing through the exact vertical centre of the screen — i.e. "crossing" the seam/needle — visibly scales up to 1.08× and brightens, then relaxes as it moves away; because the two columns run at different rates, different pairs of cards cross that centre line at different scroll moments, making the emphasis feel alive rather than mechanical. Look cards themselves are photographic: each card is a 3:4 `aspect-ratio` frame (`overflow:hidden`) holding an absolutely-positioned `<img>` at `width/height:100%; object-fit:cover; object-position:center` (`loading="lazy"`, `decoding="async"`, `draggable="false"`, `pointer-events:none`) — one quiet editorial lookbook photograph per look (a single garment on a faceless model against a seamless single-colour studio backdrop, muted desaturated grade, the lower third kept simple and slightly darker so the caption stays readable), art-directed so each backdrop sits on one of six curated colourways (cream `#F6F1EA` / charcoal `#23201F` / sand `#EFE3D6` / blush `#F0D9E0` / taupe `#DCC9B6` / ink `#201B18`) and all twelve read as one coherent seasonal palette; each card's own `background-color` is set to its colourway base so the frame holds the palette while (or if) the image loads. Each card shows a monospace look number top-left, an italic serif title + category caption bottom-left over a dark scrim, and lifts `translateY(-4px)` on hover (0.3s ease, applied to the *outer* card wrapper so it never fights the scroll-driven scale/brightness written to the inner element).

## Section 4: How it's made

Eyebrow "HOW IT'S MADE", heading "Construction, left visible." Three items in a row (stacked on mobile), alternating offset-converge direction (even from left, odd from right): "Cut — Every panel is cut on the true bias, so movement changes the silhouette."; "Seam — Visible seams face out. We don't hide the construction — we frame it."; "Finish — Raw edges are left raw where they can be. Finished only where they must." Each item has a 2px ink top rule and an italic serif title.

## Section 5: Footer

Eyebrow "GET IN TOUCH", heading "Ask before it's gone.", an italic serif mailto link "press@offsetstudio.com ↗" (hairline underline, turns accent-orchid on hover), a meta row "OFFSET STUDIO © 2026" / "INSTAGRAM — SSENSE — 032c", and a decorative dashed horizontal "stitch" line running the full content width near the bottom edge — a quiet closing echo of the seam motif.

## Performance & accessibility

No canvas/WebGL is used anywhere — the visuals are CSS gradients plus twelve static lazy-loaded `<img>` photographs, so the page is cheap to paint. The scroll-driven work (column shift, seam fill/needle, per-card scale) all happens inside a single `ScrollTrigger`'s `onUpdate`, reading at most 12 `getBoundingClientRect()` calls per tick, which is negligible even on scrub-heavy scroll. On mount, read `window.matchMedia("(prefers-reduced-motion: reduce)").matches`: if true, skip the counter-scroll ScrollTrigger entirely (both columns sit in plain flow, seam fill pinned to 0, needle pinned to top), and set every offset-converge target and the two hero idle-drift panels to their resting, non-animated state immediately — nothing on the page moves on its own, only in direct response to explicit scrolling. Re-run the column-shift measurement/setup on every `ResizeObserver` tick on the scroller so resizing across the 780px breakpoint correctly toggles between the two-column counter-scroll and the single stacked-column mobile layout. All copy, names and looks are original/fictional; no real fashion house or product is referenced.

## Action flow and content

Implement the following local planning experience for the primary CTA, in the visual language of the page. Use a real button opening a labelled modal dialog. Choosing a pricing card must preselect that exact plan. A navigation link may move to another section; a planning CTA must lead to its review flow.

Use this content for the dialog, including the option details and notes:

{
  "slug": "counter-scroll",
  "brand": "OFFSET STUDIO",
  "accent": "#746050",
  "title": "Collection enquiry",
  "intro": "Tell us which look and which use brought you here.",
  "facts": [
    [
      "Collection",
      "Twelve looks in the current index"
    ],
    [
      "Preparation",
      "Keep the look name and the material or sizing questions together."
    ],
    [
      "Next step",
      "Review samples, use and availability before an agreement."
    ]
  ],
  "groups": [
    {
      "label": "Enquiry",
      "options": [
        {
          "title": "Editorial & press",
          "detail": "Image use, credits and sample requirements for an editorial project."
        },
        {
          "title": "Buying & appointments",
          "detail": "A focused selection of looks for a showroom or buying conversation."
        }
      ]
    }
  ],
  "brief": "Name the look, the publication or appointment context, and what you need to confirm.",
  "notes": [
    "Saving this brief does not reserve a garment or grant image-use rights."
  ]
}

- Explain before input that this is a fictional brand sample: choices can be reviewed and saved, with no external submission, booking, payment or registration. Use each brand's language. Do not launch a mail client or present an installer for a fictional product.
- Give every option group a legend, inputs explicit labels and errors linked descriptions. Validate options against the allowed values, requested dates as actual dates from today onward, and free text as 10–2,000 trimmed characters. Blank spaces alone must fail. Tracking accepts the sample reference TG-2048 only.
- Focus the first invalid field and preserve every entered value. On success, show the selected option WITH its full detail, requested date or brief, relevant facts and notes. An edit action returns to the preserved form. Do not claim a real reservation, purchase or account has succeeded.
- Save the complete review as UTF-8 text. A browser download request is not proof of a file being written. On download failure, retain the full text in a labelled, selectable textarea and offer retry. Do not show a false success state.
- Constrain modal content to the viewport with internal scrolling. Support keyboard and touch, trap focus while open, close on Escape and restore focus to the exact initiating control. Keep these operations available with motion disabled.
- If a separate project-contact CTA is present, let it collect a project brief and review it locally; a product selection CTA must retain the product choice. Verify both flows independently.