Skip to content
Template Empire
Browse components

Pinned Panels

A free pinned scroll section that crossfades panels in place as you scroll.

Horizon A · 0 to 15 cm

The layer a good winter changes

Cover crops and a lighter pass move the top fifteen centimetres first. Tilth samples eight points on the same grid every quarter, so a good winter arrives as a number rather than a hunch.

  1. A

    Living topsoil

    32.6 t C/ha

    0 to 15 cm · Roots, residue, crumb structure

  2. B

    Mineral subsoil

    45.4 t C/ha

    15 to 60 cm

  3. C

    Weathered chalk

    12.8 t C/ha

    60 to 120 cm

Carbon fractions at 0 to 15 cm

Particulate
41%
Mineral-associated
47%
Inorganic
12%

Horizon B · 15 to 60 cm

Where the carbon stops moving

Below the plough line carbon binds to clay and stays. Bulk density is measured on every core instead of assumed, because that assumption is where most soil claims quietly come apart.

  1. A

    Living topsoil

    32.6 t C/ha

    0 to 15 cm

  2. B

    Mineral subsoil

    45.4 t C/ha

    15 to 60 cm · Bound to clay, slow to move

  3. C

    Weathered chalk

    12.8 t C/ha

    60 to 120 cm

Density and carbon by interval
IntervalcmDensityg/cm³Organic C%Stockt/ha
15 to 301.340.9418.9
30 to 451.410.7115.0
45 to 601.480.5211.5

Horizon C · 60 to 120 cm

Down to the chalk, then signed

The core ends in weathered chalk and so does the claim. Every horizon, density and uncertainty reaches the verifier as one file, with the raw spectra attached to each depth.

  1. A

    Living topsoil

    32.6 t C/ha

    0 to 15 cm

  2. B

    Mineral subsoil

    45.4 t C/ha

    15 to 60 cm

  3. C

    Weathered chalk

    12.8 t C/ha

    60 to 120 cm · Parent material, the core ends

2026 vintage, issued 03 Sep

612 t CO2e

Verified across 640 hectares by Brackling Assurance, against the raw spectra behind every depth.

Registry ref WLD-26-0118

Pinned Panels is a free scroll section for Next.js and React that sticks to the viewport and crossfades between two and five panels in place as the page scrolls, built with Motion and styled with Tailwind. You pass your panels as plain children, one per direct child, and the component builds the sticky frame, the scrub and the reduced motion fallback for you.

The section is pinned with CSS position: sticky inside a track that reserves the scroll distance, so scrolling stays native: wheel, keyboard and touch all carry you straight through and past the section, and nothing shifts. The pinned layout is part of the server-rendered page, so a reload or the Back button returns you to the same point in the scene. With prefers-reduced-motion enabled, or with JavaScript unavailable, the panels render as a static stack, fully visible in order.

The section pins only where your panels fit. A pinned panel is one viewport tall and clipped, and scrolling drives the crossfade rather than the panel, so on a viewport shorter than your tallest panel, such as a phone held sideways, the component leaves the section in the ordinary stacked flow instead of hiding the part of each panel that scrolling can no longer reach.

Install

Add the @te registry to your components.json once:

{
  "registries": {
    "@te": "https://templateempire.io/r/{name}.json"
  }
}

Then install the component:

npx shadcn@latest add @te/scroll-pinned-panels

Usage

import { ScrollPinnedPanels } from "@/components/scroll-pinned-panels";

export default function Page() {
  return (
    <ScrollPinnedPanels>
      <div>
        <h2>Chapter one</h2>
        <p>The section pins while the story plays.</p>
      </div>
      <div>
        <h2>Chapter two</h2>
        <p>Each panel crossfades in over the last.</p>
      </div>
      <div>
        <h2>Chapter three</h2>
        <p>Scroll on and the pin releases.</p>
      </div>
    </ScrollPinnedPanels>
  );
}

Props

PropTypeDefaultDescription
childrenReactNoderequiredPanel content. Pass 2 to 5 direct children; each direct child is rendered inside its own [data-panel] wrapper as one panel, in DOM order.
classNamestringundefinedAdditional classes merged onto the section root.

Customisation

Pinned Panels declares no tokens of its own: the section and each panel surface use the semantic background token, and everything inside a panel is your own markup, styled however you like. To retheme the section, restyle your panel content directly or override the semantic tokens it consumes from :root or any ancestor:

:root {
  --background: var(--color-neutral-900);
  --foreground: var(--color-neutral-50);
}

Frequently asked questions

Is Pinned Panels free for commercial use?
Yes. Pinned Panels is MIT licensed, so you can use it in personal and commercial projects without attribution.
Does Pinned Panels trap scrolling or hurt accessibility?
No. The crossfade is scrubbed from native scroll and never intercepts it, so mouse wheel, trackpad, touch and keyboard keys such as arrows, space and PageDown all move through and past the section uninterrupted. The track reserves the full scroll distance up front, so pinning causes no layout shift, and the crossfade animates only transforms and opacity. Every panel stays in the Tab order: moving focus into a panel scrolls the page to the point where that panel is on top, so the focused element and its focus ring are always visible. With reduced motion enabled the panels render as a static stack with nothing listening to scroll.
Why does the section scroll past instead of sticking?
The pin is CSS position: sticky, which needs a clear path to the page's scroller. If any ancestor between the section and the page sets overflow to hidden, auto or scroll, sticky stops working there; use overflow: clip on that ancestor instead, which clips the same way without creating a scroll container.
What happens on a short viewport, such as a phone in landscape?
The section stays stacked and nothing is hidden. The component measures the tallest panel's own content and only pins where one panel is at least that tall, because a pinned panel is exactly one viewport tall and clipped, and scrolling advances the crossfade rather than the panel. That measurement is retaken whenever the section changes width, not just retested: narrowing a panel rewraps your copy into a taller composition, so a width measured on a desktop says nothing about the same panels on a phone. Turn a phone from portrait to landscape, or drag a desktop window narrow, and the section drops out of the pin rather than clipping every panel; widen it again and the pin returns.
How do I change the number of panels or their content?
Pass between two and five direct children to the component; each direct child becomes one panel in DOM order. Panels are plain containers, so headings, text, images and layout inside each one are entirely your own markup and Tailwind classes, and heading levels stay under your control.

Related components

Want the full page this belongs to?Template Empire builds complete Next.js templates with this motion system throughout. Browse the paid templates that pair with these free components.Explore Template Empire templates