registry
@automindzsheets 01–77shadcn 4.x
SHEET 49

Compare Slider

component / marketing

Two images in one frame with a draggable wipe between them, driven by a real range input — so it arrives keyboard-operable, touch-capable and announced, with the frame reserved before a byte loads.

type
component
files
1
registry deps
skeleton
npm deps
lucide-react

installs

  • components/automindz/compare-slider.tsx
  • A real <input type=range>: arrow keys, Home/End, touch, and an announced percentage
  • width/height reserve the frame before a byte loads, so nothing under it jumps
  • A missing or failed image is a labelled placeholder that blocks the wipe with a reason
  • The visible handle is decorative — the focus ring is drawn through the frame's :has()
  • No stylesheet: the wipe is a clip-path driven by the input's own value

before, after, width and height have no defaults. Only the ratio of width/height is used, and it is used **before either image has loaded** — that is the entire point of them: without it the figure is 0px tall until the first byte arrives and everything under it jumps. There is no stylesheet to import and no config file. It is "use client", for one reason worth knowing: the control is a native <input type="range"> and CSS cannot read a live value (the IDL attribute is not the content attribute, so attr() never sees it change), so the wipe needs one handler and one piece of state. That input is invisible, opacity-0 rather than hidden, stretched over the whole frame with a 2px thumb so the value maps to the pointer across the full width — it brings arrow keys, Home/End, Page Up/Down, touch and an announced value that a div with a mousedown listener never gets. The visible handle is decorative and the focus ring is drawn on it through the frame's :has(). alt on each side is real alt text describing **that** picture, not "before" — that is label, and it is already on screen. A missing or failed src renders a labelled placeholder **and** blocks the control with a reason, because a wipe between a photograph and a grey box compares nothing; disabledReason does the same by hand, and both land in the figcaption as a role="status". Both images are cover-fitted to one frame so the comparison is between the same region of two pictures — set --automindz-compare-fit: contain if the pair really is two different shapes. --automindz-compare-handle (default 2px) is the handle's line width. --automindz-compare-position is **not** a dial: the input owns the position, and a stylesheet that moved the wipe without moving the announced value would leave the two disagreeing.

Variations

one state per section

01

Default, 16:9

Tab to it and use the arrow keys, Home and End: the control is a real input[type=range] laid invisibly over the whole frame, so the wipe is keyboard-operable and announced as a percentage without a single pointer listener. The focus ring lands on the handle because the element with focus is transparent. The percentage counts the before image — the side left of the seam, and the side that grows as the value does — so what is announced is what is on screen.

fluid
The reporting dashboard before the rebuild: four nav rows and one sparse chartBefore
The same dashboard after the rebuild: eight nav rows and a full chartAfter
Drag the handle, or click anywhere in the frame.
02

A mismatched pair

A square after image against a 16:9 before. Both are cover-fitted into one frame at the before image's ratio, so the seam stays over picture rather than over a letterbox bar, and the two never disagree about where a feature sits. --automindz-compare-fit is the escape hatch.

fluid
The wide dashboard, 16:9Before
A square export of the same dashboardSquare export
03

Loading

The skeleton is the frame, at its declared ratio — the same 16:9 box the images will land in. Nothing below it moves when they arrive, which is what the required width and height buy.

fluid
04

One image missing

A labelled placeholder, not the browser's broken-image glyph — and the control blocks itself with the reason in the caption, because a wipe between a photograph and a grey box compares nothing. The reason is a role=status, since a side can fail after the frame is already on screen. It stacks above the caller's own caption rather than replacing it: the reason is a state this component discovered, the caption is content somebody wrote, and aria-describedby was already promising both.

fluid
The dashboard before the rebuildBefore
After image unavailable
After
The after image didn't load, so there is nothing to compare.Ward board, March against June.
05

Disabled, with a reason

Shown, blocked, explained: one prop drives the disabled input and the sentence under it, so they cannot disagree. The handle stays where it was and dims rather than disappearing — hiding the control makes people ask where it went.

fluid
The dashboard before the rebuildBefore
The dashboard after the rebuildAfter
The after image is still rendering. This comparison unlocks when the export finishes.