Skip to content

Instantly share code, notes, and snippets.

@Michaelgathara
Created September 1, 2025 21:15
Show Gist options
  • Save Michaelgathara/111a668cfa854f41e12c2b80368ada1a to your computer and use it in GitHub Desktop.
Save Michaelgathara/111a668cfa854f41e12c2b80368ada1a to your computer and use it in GitHub Desktop.
Circular IOS-Like Time Picker
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>iOS Time Picker</title>
<style>
html,
body {
height: 100%;
margin: 0;
background: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
.timepicker {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
height: 220px;
padding: 0 8px;
}
.col {
position: relative;
height: 220px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
text-align: center;
font-size: 28px;
line-height: 44px;
scroll-snap-type: y mandatory;
-webkit-overflow-scrolling: touch;
min-width: 72px;
perspective: 600px;
transform-style: preserve-3d;
}
.col::-webkit-scrollbar {
display: none;
}
.col::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.0) 28%, rgba(0, 0, 0, 0.0) 72%, rgba(0, 0, 0, 0.85) 100%);
}
#hour {
width: 72px;
}
#minute {
width: 88px;
}
.item {
height: 44px;
scroll-snap-align: center;
scroll-snap-stop: always;
color: rgba(235, 235, 245, 0.55);
transition: color 0.12s ease, transform 0.12s ease;
transform-origin: center center;
will-change: transform, color;
}
.highlight {
pointer-events: none;
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 44px;
margin-top: -22px;
border-top: 1px solid rgba(255, 255, 255, 0.18);
border-bottom: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.06);
border-radius: 22px;
}
</style>
</head>
<body>
<div class="timepicker" id="timepicker">
<div class="col" id="hour"></div>
<div class="col" id="minute"></div>
<div class="highlight"></div>
</div>
<script>
(function () {
const ITEM_HEIGHT = 44;
const REPEATS = 9;
const columns = [
{ id: 'hour', values: Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')), loop: true },
{ id: 'minute', values: Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0')), loop: true }
];
function buildColumn(el, values, loop) {
const frag = document.createDocumentFragment();
if (loop) {
for (let r = 0; r < REPEATS; r++) {
for (let i = 0; i < values.length; i++) {
const div = document.createElement('div');
div.className = 'item';
div.textContent = values[i];
frag.appendChild(div);
}
}
} else {
for (let i = 0; i < values.length; i++) {
const div = document.createElement('div');
div.className = 'item';
div.textContent = values[i];
frag.appendChild(div);
}
}
el.appendChild(frag);
const oneSet = values.length * ITEM_HEIGHT;
const lowerBound = oneSet * 1;
const upperBound = oneSet * (REPEATS - 2);
el.scrollTop = oneSet * Math.floor(REPEATS / 2);
let isSnapping = false;
let snapTimer = null;
const finalizeSnap = () => {
isSnapping = true;
const targetIndex = Math.round((el.scrollTop + el.clientHeight / 2 - ITEM_HEIGHT / 2) / ITEM_HEIGHT);
const targetTop = targetIndex * ITEM_HEIGHT - (el.clientHeight / 2 - ITEM_HEIGHT / 2);
el.scrollTo({ top: targetTop, behavior: 'smooth' });
setTimeout(() => { isSnapping = false; }, 240);
};
const loopAndHighlight = () => {
if (loop) {
if (el.scrollTop < lowerBound) {
el.scrollTop += oneSet * (REPEATS - 2);
} else if (el.scrollTop > upperBound) {
el.scrollTop -= oneSet * (REPEATS - 2);
}
}
const center = el.scrollTop + el.clientHeight / 2;
const items = el.querySelectorAll('.item');
for (let i = 0; i < items.length; i++) {
const item = items[i];
const box = item.offsetTop + ITEM_HEIGHT / 2;
const dist = Math.abs(center - box);
if (dist < ITEM_HEIGHT / 2) {
item.style.color = '#ffffff';
item.style.transform = 'scale(1.12) rotateX(0deg) translateZ(0)';
item.style.fontWeight = '600';
} else {
const offset = (box - center) / ITEM_HEIGHT;
const clamped = Math.max(-2, Math.min(2, offset));
const abs = Math.abs(clamped);
const rotate = clamped * 12;
const translateZ = -10 * abs;
const alpha = 0.55 - Math.min(0.35, abs * 0.18);
item.style.color = `rgba(235,235,245,${alpha.toFixed(3)})`;
item.style.transform = `scale(${1 - Math.min(0.06, abs * 0.03)}) rotateX(${rotate}deg) translateZ(${translateZ}px)`;
item.style.fontWeight = '400';
}
}
};
const onScroll = () => {
loopAndHighlight();
if (isSnapping) return;
clearTimeout(snapTimer);
snapTimer = setTimeout(finalizeSnap, 120);
};
el.addEventListener('scroll', onScroll, { passive: true });
loopAndHighlight();
}
columns.forEach(cfg => buildColumn(document.getElementById(cfg.id), cfg.values, cfg.loop));
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment