Scissor Lift
import { useState, type CSSProperties } from 'react';
const L = 118; // arm length
const STAGES = 3;
const BASE = 168; // top of the chassis
const X0 = 22; // the fixed pivots
const S_MIN = 0.06; // sine of the arm angle, folded
const S_MAX = 0.36; // and fully raised
const SPAN = (STAGES * L * (S_MAX - S_MIN)).toFixed(1);
const RISE = (STAGES * L * S_MIN).toFixed(1);
// Every length comes from one registered number, so the arms, pins, platform and
// tag all move together when it eases. Height is linear in the slider value: the
// sine of the arm angle runs evenly, and the angle is its arcsine.
const css = `
@property --slider-scissor-lift-v { syntax: '<number>'; inherits: true; initial-value: 0; }
.slider-scissor-lift { --s: calc(${S_MIN} + var(--slider-scissor-lift-v) * ${(S_MAX - S_MIN).toFixed(2)}); --t: asin(var(--s)); --h: calc(${L}px * var(--s)); --w: calc(${L}px * cos(var(--t))); position: relative; display: grid; grid-template-columns: 170px 1fr; width: 230px; height: 196px; transition: --slider-scissor-lift-v 0.45s cubic-bezier(0.3, 1.15, 0.5, 1); }
.slider-scissor-lift__rig { position: relative; }
.slider-scissor-lift__chassis { position: absolute; left: 6px; top: ${BASE}px; width: 152px; height: 14px; border-radius: 4px; background: linear-gradient(light-dark(#5f626a, #a7abb2), light-dark(#cbced3, #393c41)); box-shadow: inset 0 1px 0 light-dark(#50535b, #c6c8cd); }
.slider-scissor-lift__wheel { position: absolute; top: ${BASE + 10}px; width: 16px; height: 16px; border-radius: 50%; background: radial-gradient(circle, light-dark(#5f626a, #a7abb2) 0 3px, light-dark(#dbdde1, #2e3035) 3.5px); }
.slider-scissor-lift__arm { position: absolute; left: ${X0}px; width: ${L}px; height: 6px; margin-top: -3px; border-radius: 3px; transform-origin: 3px 50%; background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#6d7179, #7d8087)); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
.slider-scissor-lift__arm--up { top: calc(${BASE}px - var(--k) * var(--h)); rotate: calc(-1 * var(--t)); }
.slider-scissor-lift__arm--down { top: calc(${BASE}px - (var(--k) + 1) * var(--h)); rotate: var(--t); z-index: 1; }
.slider-scissor-lift__pin { position: absolute; left: calc(${X0 + 3}px + var(--w) / 2 - 4px); top: calc(${BASE}px - (var(--k) + 0.5) * var(--h) - 4px); width: 8px; height: 8px; border-radius: 50%; background: light-dark(#42454c, #e5e6e9); box-shadow: inset 0 0 0 2px light-dark(#6d7179, #7d8087); z-index: 2; }
/* The platform, its rail and a worker standing on it. */
.slider-scissor-lift__deck { position: absolute; left: 10px; width: 148px; top: calc(${BASE}px - ${STAGES} * var(--h) - 9px); height: 9px; border-radius: 2px; background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#6d7179, #7d8087)); z-index: 3; }
.slider-scissor-lift__deck::before { content: ''; position: absolute; left: 2px; right: 2px; bottom: 9px; height: 22px; border: 2.5px solid light-dark(#6d7179, #7d8087); border-bottom: 0; border-radius: 2px 2px 0 0; background: linear-gradient(90deg, transparent 0 calc(50% - 1.25px), light-dark(#6d7179, #7d8087) calc(50% - 1.25px) calc(50% + 1.25px), transparent 0); }
.slider-scissor-lift__worker { position: absolute; left: 96px; bottom: 9px; width: 12px; height: 32px; }
.slider-scissor-lift__worker::before { content: ''; position: absolute; left: 1px; bottom: 0; width: 10px; height: 22px; border-radius: 5px 5px 2px 2px; background: light-dark(#42454c, #e5e6e9); }
.slider-scissor-lift__worker::after { content: ''; position: absolute; left: 1px; top: 0; width: 10px; height: 10px; border-radius: 50%; background: light-dark(#50535b, #c6c8cd); box-shadow: 0 -2px 0 -1px light-dark(#373a40, #f1f2f3); }
.slider-scissor-lift__crate { position: absolute; left: 30px; bottom: 9px; width: 22px; height: 18px; border-radius: 2px; background: light-dark(#cbced3, #393c41); box-shadow: inset 0 0 0 2px light-dark(#6d7179, #7d8087); }
/* The height scale; its handle rides level with the deck. */
.slider-scissor-lift__scale { position: relative; }
.slider-scissor-lift__line { position: absolute; left: 14px; top: calc(${BASE}px - ${RISE}px - ${SPAN}px - 9px); height: ${SPAN}px; width: 2px; border-radius: 1px; background: light-dark(#dbdde1, #2e3035); }
.slider-scissor-lift__tick { position: absolute; left: 10px; width: 10px; height: 1px; background: light-dark(#6d7179, #7d8087); }
.slider-scissor-lift__tick b { position: absolute; left: 14px; top: -6px; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-scissor-lift__tag { position: absolute; left: 6px; top: calc(${BASE}px - ${STAGES} * var(--h) - 9px); translate: 0 -50%; padding: 3px 6px; border-radius: 5px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#f3f4f7, #1a1c1f); font-size: 11px; font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.5)); }
.slider-scissor-lift__tag::before { content: ''; position: absolute; left: -5px; top: 50%; translate: 0 -50%; border: 5px solid transparent; border-left: 0; border-right-color: light-dark(#373a40, #f1f2f3); }
.slider-scissor-lift:has(.slider-scissor-lift__input:focus-visible) .slider-scissor-lift__tag { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
/* A horizontal range turned a quarter, over the whole lift: drag the deck up and down. */
.slider-scissor-lift__input { position: absolute; left: 115px; top: calc(${BASE}px - ${RISE}px - ${SPAN}px / 2 - 9px); width: calc(${SPAN}px + 24px); height: 230px; margin: 0; translate: -50% -50%; rotate: -90deg; opacity: 0; cursor: ns-resize; -webkit-appearance: none; appearance: none; background: none; }
.slider-scissor-lift__input::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 230px; }
.slider-scissor-lift__input::-moz-range-thumb { width: 24px; height: 230px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-scissor-lift { transition: none; }
}
`;
export function Preview() {
const [v, setV] = useState(55);
const metres = (v / 10).toFixed(1);
return (
<>
<style href="kit-sliders-scissor-lift" precedence="kit">
{css}
</style>
<div className="slider-scissor-lift" style={{ '--slider-scissor-lift-v': v / 100 } as CSSProperties}>
<div className="slider-scissor-lift__rig" aria-hidden="true">
{Array.from({ length: STAGES }, (_, k) => (
<span key={k} style={{ '--k': k } as CSSProperties}>
<i className="slider-scissor-lift__arm slider-scissor-lift__arm--up" />
<i className="slider-scissor-lift__arm slider-scissor-lift__arm--down" />
<i className="slider-scissor-lift__pin" />
</span>
))}
<span className="slider-scissor-lift__deck">
<span className="slider-scissor-lift__crate" />
<span className="slider-scissor-lift__worker" />
</span>
<span className="slider-scissor-lift__chassis" />
<span className="slider-scissor-lift__wheel" style={{ left: 16 }} />
<span className="slider-scissor-lift__wheel" style={{ left: 132 }} />
</div>
<div className="slider-scissor-lift__scale">
<span className="slider-scissor-lift__line" aria-hidden="true" />
{[0, 2, 4, 6, 8, 10].map((m) => (
<span key={m} className="slider-scissor-lift__tick" style={{ top: `calc(${BASE}px - ${RISE}px - 9px - ${m / 10} * ${SPAN}px)` }} aria-hidden="true">
<b>{m}</b>
</span>
))}
<span className="slider-scissor-lift__tag" aria-hidden="true">
{metres} m
</span>
</div>
<input className="slider-scissor-lift__input" type="range" min={0} max={100} value={v} onChange={(e) => setV(+e.target.value)} aria-label="Platform height" aria-valuetext={`${metres} metres`} aria-orientation="vertical" />
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderScissorLift(el) on the mounted element instead.
<div class="slider-scissor-lift" style="--slider-scissor-lift-v:0.55">
<div class="slider-scissor-lift__rig" aria-hidden="true">
<span style="--k:0">
<i class="slider-scissor-lift__arm slider-scissor-lift__arm--up"></i>
<i class="slider-scissor-lift__arm slider-scissor-lift__arm--down"></i>
<i class="slider-scissor-lift__pin"></i>
</span>
<span style="--k:1">
<i class="slider-scissor-lift__arm slider-scissor-lift__arm--up"></i>
<i class="slider-scissor-lift__arm slider-scissor-lift__arm--down"></i>
<i class="slider-scissor-lift__pin"></i>
</span>
<span style="--k:2">
<i class="slider-scissor-lift__arm slider-scissor-lift__arm--up"></i>
<i class="slider-scissor-lift__arm slider-scissor-lift__arm--down"></i>
<i class="slider-scissor-lift__pin"></i>
</span>
<span class="slider-scissor-lift__deck">
<span class="slider-scissor-lift__crate"></span>
<span class="slider-scissor-lift__worker"></span>
</span>
<span class="slider-scissor-lift__chassis"></span>
<span class="slider-scissor-lift__wheel" style="left:16px"></span>
<span class="slider-scissor-lift__wheel" style="left:132px"></span>
</div>
<div class="slider-scissor-lift__scale">
<span class="slider-scissor-lift__line" aria-hidden="true"></span>
<span class="slider-scissor-lift__tick" style="top:calc(168px - 21.2px - 9px - 0 * 106.2px)" aria-hidden="true">
<b>0</b>
</span>
<span class="slider-scissor-lift__tick" style="top:calc(168px - 21.2px - 9px - 0.2 * 106.2px)" aria-hidden="true">
<b>2</b>
</span>
<span class="slider-scissor-lift__tick" style="top:calc(168px - 21.2px - 9px - 0.4 * 106.2px)" aria-hidden="true">
<b>4</b>
</span>
<span class="slider-scissor-lift__tick" style="top:calc(168px - 21.2px - 9px - 0.6 * 106.2px)" aria-hidden="true">
<b>6</b>
</span>
<span class="slider-scissor-lift__tick" style="top:calc(168px - 21.2px - 9px - 0.8 * 106.2px)" aria-hidden="true">
<b>8</b>
</span>
<span class="slider-scissor-lift__tick" style="top:calc(168px - 21.2px - 9px - 1 * 106.2px)" aria-hidden="true">
<b>10</b>
</span>
<span class="slider-scissor-lift__tag" aria-hidden="true">5.5 m</span>
</div>
<input class="slider-scissor-lift__input" type="range" min="0" max="100" aria-label="Platform height" aria-valuetext="5.5 metres" aria-orientation="vertical" value="55"/>
</div>@property --slider-scissor-lift-v { syntax: '<number>'; inherits: true; initial-value: 0; }
.slider-scissor-lift { --s: calc(0.06 + var(--slider-scissor-lift-v) * 0.30); --t: asin(var(--s)); --h: calc(118px * var(--s)); --w: calc(118px * cos(var(--t))); position: relative; display: grid; grid-template-columns: 170px 1fr; width: 230px; height: 196px; transition: --slider-scissor-lift-v 0.45s cubic-bezier(0.3, 1.15, 0.5, 1); }
.slider-scissor-lift__rig { position: relative; }
.slider-scissor-lift__chassis { position: absolute; left: 6px; top: 168px; width: 152px; height: 14px; border-radius: 4px; background: linear-gradient(light-dark(#5f626a, #a7abb2), light-dark(#cbced3, #393c41)); box-shadow: inset 0 1px 0 light-dark(#50535b, #c6c8cd); }
.slider-scissor-lift__wheel { position: absolute; top: 178px; width: 16px; height: 16px; border-radius: 50%; background: radial-gradient(circle, light-dark(#5f626a, #a7abb2) 0 3px, light-dark(#dbdde1, #2e3035) 3.5px); }
.slider-scissor-lift__arm { position: absolute; left: 22px; width: 118px; height: 6px; margin-top: -3px; border-radius: 3px; transform-origin: 3px 50%; background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#6d7179, #7d8087)); box-shadow: 0 1px 2px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.4)); }
.slider-scissor-lift__arm--up { top: calc(168px - var(--k) * var(--h)); rotate: calc(-1 * var(--t)); }
.slider-scissor-lift__arm--down { top: calc(168px - (var(--k) + 1) * var(--h)); rotate: var(--t); z-index: 1; }
.slider-scissor-lift__pin { position: absolute; left: calc(25px + var(--w) / 2 - 4px); top: calc(168px - (var(--k) + 0.5) * var(--h) - 4px); width: 8px; height: 8px; border-radius: 50%; background: light-dark(#42454c, #e5e6e9); box-shadow: inset 0 0 0 2px light-dark(#6d7179, #7d8087); z-index: 2; }
/* The platform, its rail and a worker standing on it. */
.slider-scissor-lift__deck { position: absolute; left: 10px; width: 148px; top: calc(168px - 3 * var(--h) - 9px); height: 9px; border-radius: 2px; background: linear-gradient(light-dark(#50535b, #c6c8cd), light-dark(#6d7179, #7d8087)); z-index: 3; }
.slider-scissor-lift__deck::before { content: ''; position: absolute; left: 2px; right: 2px; bottom: 9px; height: 22px; border: 2.5px solid light-dark(#6d7179, #7d8087); border-bottom: 0; border-radius: 2px 2px 0 0; background: linear-gradient(90deg, transparent 0 calc(50% - 1.25px), light-dark(#6d7179, #7d8087) calc(50% - 1.25px) calc(50% + 1.25px), transparent 0); }
.slider-scissor-lift__worker { position: absolute; left: 96px; bottom: 9px; width: 12px; height: 32px; }
.slider-scissor-lift__worker::before { content: ''; position: absolute; left: 1px; bottom: 0; width: 10px; height: 22px; border-radius: 5px 5px 2px 2px; background: light-dark(#42454c, #e5e6e9); }
.slider-scissor-lift__worker::after { content: ''; position: absolute; left: 1px; top: 0; width: 10px; height: 10px; border-radius: 50%; background: light-dark(#50535b, #c6c8cd); box-shadow: 0 -2px 0 -1px light-dark(#373a40, #f1f2f3); }
.slider-scissor-lift__crate { position: absolute; left: 30px; bottom: 9px; width: 22px; height: 18px; border-radius: 2px; background: light-dark(#cbced3, #393c41); box-shadow: inset 0 0 0 2px light-dark(#6d7179, #7d8087); }
/* The height scale; its handle rides level with the deck. */
.slider-scissor-lift__scale { position: relative; }
.slider-scissor-lift__line { position: absolute; left: 14px; top: calc(168px - 21.2px - 106.2px - 9px); height: 106.2px; width: 2px; border-radius: 1px; background: light-dark(#dbdde1, #2e3035); }
.slider-scissor-lift__tick { position: absolute; left: 10px; width: 10px; height: 1px; background: light-dark(#6d7179, #7d8087); }
.slider-scissor-lift__tick b { position: absolute; left: 14px; top: -6px; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-scissor-lift__tag { position: absolute; left: 6px; top: calc(168px - 3 * var(--h) - 9px); translate: 0 -50%; padding: 3px 6px; border-radius: 5px; background: light-dark(#373a40, #f1f2f3); color: light-dark(#f3f4f7, #1a1c1f); font-size: 11px; font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.5)); }
.slider-scissor-lift__tag::before { content: ''; position: absolute; left: -5px; top: 50%; translate: 0 -50%; border: 5px solid transparent; border-left: 0; border-right-color: light-dark(#373a40, #f1f2f3); }
.slider-scissor-lift:has(.slider-scissor-lift__input:focus-visible) .slider-scissor-lift__tag { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
/* A horizontal range turned a quarter, over the whole lift: drag the deck up and down. */
.slider-scissor-lift__input { position: absolute; left: 115px; top: calc(168px - 21.2px - 106.2px / 2 - 9px); width: calc(106.2px + 24px); height: 230px; margin: 0; translate: -50% -50%; rotate: -90deg; opacity: 0; cursor: ns-resize; -webkit-appearance: none; appearance: none; background: none; }
.slider-scissor-lift__input::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 230px; }
.slider-scissor-lift__input::-moz-range-thumb { width: 24px; height: 230px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-scissor-lift { transition: none; }
}function initSliderScissorLift(root) {
var input = root.querySelector('.slider-scissor-lift__input');
var tag = root.querySelector('.slider-scissor-lift__tag');
function update() {
var v = +input.value;
var metres = (v / 10).toFixed(1);
// One registered number drives the arm angle, the deck and the tag; CSS eases it.
root.style.setProperty('--slider-scissor-lift-v', v / 100);
tag.textContent = metres + ' m';
input.setAttribute('aria-valuetext', metres + ' metres');
}
input.addEventListener('input', update);
update();
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.slider-scissor-lift').forEach(initSliderScissorLift);Goal: Create a "Scissor Lift" slider.
Visual behavior: Drag the platform up the scale and the scissor arms open stage by stage under it, the sliding ends running along their tracks.
Technique:
- One real <input type="range"> turned a quarter lies over the whole lift, so dragging up raises the platform. It sets a single registered number.
- That number is registered with @property, so it eases. The sine of the arm angle runs evenly with it, so the height is linear; the angle is its asin() and the span its cos().
- Every arm, pin, the deck and the height tag are placed from those values with calc(), so the whole mechanism moves as one.
- A worker and a crate ride the deck, and the reading rides beside it on the scale.
Accessibility: A native range input named "Platform height" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the lift moves without easing.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Scissor Lift" slider.
Visual behavior: Drag the platform up the scale and the scissor arms open stage by stage under it, the sliding ends running along their tracks.
Technique:
- One real <input type="range"> turned a quarter lies over the whole lift, so dragging up raises the platform. It sets a single registered number.
- That number is registered with @property, so it eases. The sine of the arm angle runs evenly with it, so the height is linear; the angle is its asin() and the span its cos().
- Every arm, pin, the deck and the height tag are placed from those values with calc(), so the whole mechanism moves as one.
- A worker and a crate ride the deck, and the reading rides beside it on the scale.
Accessibility: A native range input named "Platform height" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the lift moves without easing.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
Drag the platform up the scale and the scissor arms open stage by stage under it, the sliding ends running along their tracks.
- Dependencies
- None
- Difficulty
- Advanced
- Triggers
- state change, spring
- Techniques
- css property, css variables, css transition
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input named "Platform height" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the lift moves without easing.