Scale end lift

End icons jump up when the thumb arrives and settle back slowly. The motion is a CSS transition.

v0.1.0
🍰
πŸƒ
πŸŽ“
Beginner

Install

Requires a project with shadcn/ui initialized. Installs all 2 files and any listed registry dependencies.

npx shadcn@latest add https://manglekuo.com/showcase/r/scale-end-lift.json
  • @ui/scale-end-lift.tsx
  • @ui/scale-end-lift.css

Documentation

Scale end lift

The icons at the ends of a scale jump up when the thumb arrives, then settle back down when it leaves. The height is not tied to distance. Closer does not mean taller.

How it works

Two jobs, kept apart.

The first is a step. isAtScaleEnd measures distance from an end as a fraction of the track. Inside the last 5% (SCALE_LIFT_REACH), that mark is up. Outside it, the mark is down. There is no slope between those states. A ramp that grew with proximity is what made an earlier version feel like a 45Β° line. This target is a square wave: resting, or fully raised.

The second job is the transition, and it has two timings because CSS uses the transition on the state you are entering.

.scale-end-mark {  translate: 0 0.25rem;  transition: translate 500ms cubic-bezier(0.22, 1, 0.36, 1);}Β .scale-end-mark[data-lifted] {  translate: 0 -0.125rem;  transition: translate 150ms cubic-bezier(0.19, 1, 0.22, 1);}Language: css

Adding data-lifted plays the short, steep ease-out, so the icon jumps up. Removing it plays the longer ease-out, so the icon drops and settles. Both states sit the same small distance above the track; only the 6px between them moves.

Transition translate, not transform. Utility classes such as Tailwind’s translate-y-* write the separate translate property. A transition that only lists transform never sees the change, and the icon snaps.

A transition starts from the current value. Reverse while the icon is still in the air and it turns around from there, instead of restarting the curve.

Why this works without JavaScript

Nothing interpolates the icon. There is no animation frame and no motion library. The browser runs transition: translate.

A native range input already slides with scripting off. The only script this needs is a flag when the thumb crosses the step. The movement stays in the stylesheet:

<input type="range" min="0" max="100" value="50" oninput="  start.toggleAttribute('data-lifted', this.valueAsNumber < 5)  end.toggleAttribute('data-lifted', this.valueAsNumber > 95)">Language: html

Leave the handler off and the range still works. The icons stay down until something sets data-lifted β€” HTML, a render, or that one line. The jump is the same in each case.

prefers-reduced-motion: reduce turns the transition off. The mark changes place at once instead of travelling.

Usage

import { ScaleEndLift } from '@/components/ui/scale-end-lift';Β <ScaleEndLift  label="Height"  value={value}  min={0}  max={100}  onValueChange={setValue}/>Language: jsx

Props

  • value (number): Current thumb position.
  • min / max (number, default 0 / 100): Track ends.
  • step (number, default 1): Native range step.
  • onValueChange ((value: number) => void): Fired by the range input.
  • label (string): Accessible name for the input.
  • start / end (ReactNode): Marks at the two ends. Defaults are a house and a tower.