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).
win 10.220
win 20.060
hero
ad
embed
carousel
sticky
footer
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.jsonUsage
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 thecb-labelstyle) anddescription. 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
labelslot). - Footer. Renders the reported CLS — the largest session window — against
goodThreshold.
Understanding the component
- Session grouping. A new window opens whenever the gap to the previous shift exceeds
gapMsor the span from the first shift exceedswindowMs. Every other shift extends the current window. - 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.
- Verdicts. Per-window and overall verdicts use the Web Vitals cutoffs: at most
goodThresholdisgood, at mostpoorThresholdisneeds-improvement, above ispoor. - Motion. Bands ease in with
SPRINGS.smooth, markers pop withSPRINGS.snap, and the CLS value re-animates whenever the score changes. All animations short-circuit underprefers-reduced-motion.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
shifts | ClsSessionShift[] | required | Ordered list of layout-shift entries. |
title | ReactNode | — | Optional heading above the timeline. |
description | ReactNode | — | Optional sub-headline under the title. |
durationMs | number | derived from last shift | Total timeline window. |
windowMs | number | 5000 | Maximum total span of a session window. |
gapMs | number | 1000 | Maximum gap between consecutive shifts in the same window. |
goodThreshold | number | 0.1 | At-or-below threshold for the good verdict. |
poorThreshold | number | 0.25 | At-or-below threshold for the needs-improvement verdict. |
hideCallout | boolean | false | Hide the sum-vs-largest callout chip. |
hideTotal | boolean | false | Hide the cumulative CLS footer. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
ClsSessionShift
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
label | ReactNode | Visible marker label. Falls back to id. |
time | number | Milliseconds from the start of the recording. |
score | number | Per-entry shift score, typically impactFraction × distance. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="cls-session-windows"anddata-cb-verdictmirroring the page-level verdict. - The timeline carries
role="img"with a summaryaria-labelso screen readers get the headline (window count, largest score, span) without having to walk individual bands. - Each session band carries
data-cb-verdictanddata-cb-largest, and exposes a descriptivearia-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 pulledclsWindows,clsValue, and a replay key out of aCwvContextand 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 plainshifts: { 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.