CLS Session Windows

A timeline panel that shows why the naive "sum every shift" total isn't Cumulative Layout Shift. CLS reports the largest session window: a run of shifts no more than gapMs apart (1s by default) and no longer than windowMs end-to-end (5s by default). The widget groups the input shifts, shades each session band by its summed score, drops a marker at every shift, and surfaces both the naive sum and the reported CLS so learners can see the gap.

Preview

CLS session windows

Six shifts captured over the first six seconds of page load.

Sum of every shift is 0.280, but CLS reports only the largest session window (5s window, 1s gap).
CLS (largest session window)target 0.1000.220
Customize
Shift scores
0.08
0.06
0.04
Window rules
5000
1000
Chrome

Installation

npx shadcn@latest add https://craftbits.dev/r/cls-session-windows.json

Usage

import { ClsSessionWindows } from "@craft-bits/core";
 
<ClsSessionWindows
  title="CLS session windows"
  durationMs={6000}
  shifts={[
    { id: "hero", label: "hero", time: 420, score: 0.08 },
    { id: "ad", label: "ad", time: 980, score: 0.06 },
    { id: "embed", label: "embed", time: 1850, score: 0.05 },
    { id: "sticky", label: "sticky", time: 4400, score: 0.04 },
  ]}
/>

Tune the window and gap to demonstrate a stricter local rule or the spec's older single-window behaviour:

<ClsSessionWindows
  shifts={shifts}
  windowMs={3000}
  gapMs={500}
/>

Anatomy

  • Header. Optional title (rendered with the cb-label style) and description. Omit both for a chromeless panel.
  • Callout. A one-line bug catcher that contrasts the sum of every shift with the largest window total.
  • Timeline. A track with second ticks along the bottom. Session windows render as coloured bands; the band with the highest total picks up a subtle ring and reads as data-cb-largest="true".
  • Markers. Every shift drops a dot under its band, labelled with the shift id (or its label slot).
  • Footer. Renders the reported CLS — the largest session window — against goodThreshold.

Understanding the component

  1. Session grouping. A new window opens whenever the gap to the previous shift exceeds gapMs or the span from the first shift exceeds windowMs. Every other shift extends the current window.
  2. Reported CLS. CLS = max over all windows of the per-window sum. The widget computes the naive sum for the callout and the largest-window total for the footer.
  3. Verdicts. Per-window and overall verdicts use the Web Vitals cutoffs: at most goodThreshold is good, at most poorThreshold is needs-improvement, above is poor.
  4. Motion. Bands ease in with SPRINGS.smooth, markers pop with SPRINGS.snap, and the CLS value re-animates whenever the score changes. All animations short-circuit under prefers-reduced-motion.

Props

PropTypeDefaultDescription
shiftsClsSessionShift[]requiredOrdered list of layout-shift entries.
titleReactNodeOptional heading above the timeline.
descriptionReactNodeOptional sub-headline under the title.
durationMsnumberderived from last shiftTotal timeline window.
windowMsnumber5000Maximum total span of a session window.
gapMsnumber1000Maximum gap between consecutive shifts in the same window.
goodThresholdnumber0.1At-or-below threshold for the good verdict.
poorThresholdnumber0.25At-or-below threshold for the needs-improvement verdict.
hideCalloutbooleanfalseHide the sum-vs-largest callout chip.
hideTotalbooleanfalseHide the cumulative CLS footer.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

ClsSessionShift

FieldTypeDescription
idstringStable identifier.
labelReactNodeVisible marker label. Falls back to id.
timenumberMilliseconds from the start of the recording.
scorenumberPer-entry shift score, typically impactFraction × distance.

Accessibility

  • The wrapper is a <section> with data-cb-edu="cls-session-windows" and data-cb-verdict mirroring the page-level verdict.
  • The timeline carries role="img" with a summary aria-label so screen readers get the headline (window count, largest score, span) without having to walk individual bands.
  • Each session band carries data-cb-verdict and data-cb-largest, and exposes a descriptive aria-label (window index and score).
  • The CLS footer is wrapped in aria-live="polite" so updates announce without interrupting the user.
  • Animations short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/perf-cwv/ui/ClsSessionWindows.tsx). The original pulled clsWindows, clsValue, and a replay key out of a CwvContext and ran a setTimeout-driven step animation that revealed shifts in lesson order. This rewrite drops the context, the replay button, and the project-specific markup, and computes session windows in-component from a plain shifts: { id, time, score }[] shape. Window and gap thresholds are now props (defaulting to the Web Vitals spec values of 5s / 1s) so the same widget can demonstrate stricter local rules without forking.