09Long-press reveal — the menu appears once the pressed duration value passes a threshold
Once the duration value the screen has been pressed passes a set threshold value, the menu appears anchored to the pressed coordinate value, and releasing before that threshold shows no menu at all.
Best forLong-pressing a list item on mobile, long-pressing an image on a touch screen
tickPress(ts)HOLD_MS = 620 / CIRC = 100.5stroke-dashoffset = CIRC * (1 - prog)

<main class="stage">
<div class="cm is-demo" id="cm">
<div class="cm__field" id="cmField">
<span class="cm__hint">Long-press</span>
<svg class="cm__ring" id="cmRing" viewBox="0 0 40 40">
<circle class="cm__ring-bg" cx="20" cy="20" r="16"></circle>
<circle class="cm__ring-fg" id="cmRingFg" cx="20" cy="20" r="16" stroke-dasharray="100.5" stroke-dashoffset="100.5"></circle>
</svg>
</div>
<div class="cm__menu" id="cmMenu" role="menu">
<button class="cm__item" type="button" role="menuitem">
<span class="cm__ic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg></span>
<span>Copy</span>
</button>
<button class="cm__item" type="button" role="menuitem">
<span class="cm__ic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="M208 48v168a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h40a39.83 39.83 0 0 0-8 24v8h80v-8a39.83 39.83 0 0 0-8-24h40a8 8 0 0 1 8 8" opacity=".2"/><path d="M200 32h-36.26a47.92 47.92 0 0 0-71.48 0H56a16 16 0 0 0-16 16v168a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m-72 0a32 32 0 0 1 32 32H96a32 32 0 0 1 32-32m72 184H56V48h26.75A47.9 47.9 0 0 0 80 64v8a8 8 0 0 0 8 8h80a8 8 0 0 0 8-8v-8a47.9 47.9 0 0 0-2.75-16H200Z"/></g></svg></span>
<span>Paste</span>
</button>
<button class="cm__item" type="button" role="menuitem">
<span class="cm__ic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="m221.66 90.34l-56 56l-40-40l56-56l29.66-29.66a8 8 0 0 1 11.31 0L221.66 79a8 8 0 0 1 0 11.34" opacity=".2"/><path d="m227.31 73.37l-44.68-44.69a16 16 0 0 0-22.63 0L36.69 152A15.86 15.86 0 0 0 32 163.31V208a16 16 0 0 0 16 16h44.69a15.86 15.86 0 0 0 11.31-4.69L227.31 96a16 16 0 0 0 0-22.63M92.69 208H48v-44.69l88-88L180.69 120ZM192 108.68L147.31 64l24-24L216 84.68Z"/></g></svg></span>
<span>Rename</span>
</button>
<button class="cm__item cm__item--danger" type="button" role="menuitem">
<span class="cm__ic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="M200 56v152a8 8 0 0 1-8 8H64a8 8 0 0 1-8-8V56Z" opacity=".2"/><path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16M96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0m48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0"/></g></svg></span>
<span>Delete</span>
</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 포인터가 눌려 있는 시간을 requestAnimationFrame 으로 매 프레임 재서 0~1 진행률로
// 정규화하고, 그 값을 링 SVG 의 stroke-dashoffset 에 그대로 대입해 원형 진행 표시를 그린다.
@include stage($stage-ink);
.cm {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
.cm__field {
position: relative;
width: clamp(190px, 60vw, 260px);
height: 130px;
border-radius: $r-card;
background: $subject-mint;
color: $subject-ink;
display: grid;
place-items: center;
padding: $sp-3;
box-sizing: border-box;
font: 600 14px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: center;
user-select: none;
overflow: hidden;
}
.cm__hint { opacity: .75; font-weight: 500; font-size: 12px; }
.cm__ring {
position: absolute;
width: 40px;
height: 40px;
transform: rotate(-90deg);
opacity: 0;
circle {
fill: none;
stroke-width: 4;
}
.cm__ring-bg { stroke: rgba($subject-ink, .18); }
.cm__ring-fg { stroke: $subject-ink; stroke-linecap: round; }
&.is-active { opacity: 1; }
}
.cm__menu {
position: fixed;
top: 0;
left: 0;
min-width: 168px;
background: $stage-paper;
border-radius: $r-sm;
box-shadow: $shadow-raised;
padding: $sp-1;
display: grid;
gap: $sp-1;
opacity: 0;
transform: scale(.9);
transform-origin: center;
pointer-events: none;
transition: opacity $dur-quick $ease-pop, transform $dur-quick $ease-pop;
z-index: 5;
&.is-open { opacity: 1; transform: scale(1); pointer-events: auto; }
}
.cm__item {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2 $sp-3;
border: 0;
background: none;
border-radius: $r-xs;
font: 500 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $subject-ink;
cursor: pointer;
text-align: left;
&:hover, &:focus-visible { background: rgba($subject-ink, .08); }
&--danger { color: $error; }
}
.cm__ic {
width: 16px;
height: 16px;
flex: none;
color: $subject-blue;
svg { display: block; width: 100%; height: 100%; }
}
.cm__item--danger .cm__ic { color: $error; }
@media (max-width: 340px) {
.cm__field { width: 176px; height: 108px; font-size: 12px; padding: $sp-2; }
.cm__menu { min-width: 144px; }
.cm__item { padding: $sp-1 $sp-2; font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.cm__menu { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cm {
position: relative;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
.cm__field {
position: relative;
width: clamp(190px, 60vw, 260px);
height: 130px;
border-radius: 16px;
background: #4cd4a6;
color: #17141a;
display: grid;
place-items: center;
padding: 12px;
box-sizing: border-box;
font: 600 14px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: center;
user-select: none;
overflow: hidden;
}
.cm__hint {
opacity: 0.75;
font-weight: 500;
font-size: 12px;
}
.cm__ring {
position: absolute;
width: 40px;
height: 40px;
transform: rotate(-90deg);
opacity: 0;
}
.cm__ring circle {
fill: none;
stroke-width: 4;
}
.cm__ring .cm__ring-bg {
stroke: rgba(23, 20, 26, 0.18);
}
.cm__ring .cm__ring-fg {
stroke: #17141a;
stroke-linecap: round;
}
.cm__ring.is-active {
opacity: 1;
}
.cm__menu {
position: fixed;
top: 0;
left: 0;
min-width: 168px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 4px;
display: grid;
gap: 4px;
opacity: 0;
transform: scale(0.9);
transform-origin: center;
pointer-events: none;
transition: opacity 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
z-index: 5;
}
.cm__menu.is-open {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.cm__item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 0;
background: none;
border-radius: 4px;
font: 500 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
cursor: pointer;
text-align: left;
}
.cm__item:hover, .cm__item:focus-visible {
background: rgba(23, 20, 26, 0.08);
}
.cm__item--danger {
color: #c2361a;
}
.cm__ic {
width: 16px;
height: 16px;
flex: none;
color: #2f6df6;
}
.cm__ic svg {
display: block;
width: 100%;
height: 100%;
}
.cm__item--danger .cm__ic {
color: #c2361a;
}
@media (max-width: 340px) {
.cm__field {
width: 176px;
height: 108px;
font-size: 12px;
padding: 8px;
}
.cm__menu {
min-width: 144px;
}
.cm__item {
padding: 4px 8px;
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.cm__menu {
transition: none;
}
}(function () {
var field = document.getElementById('cmField');
var menu = document.getElementById('cmMenu');
var ring = document.getElementById('cmRing');
var ringFg = document.getElementById('cmRingFg');
var root = document.getElementById('cm');
var GAP = 6;
var HOLD_MS = 620;
var CIRC = 100.5;
function positionAt(x, y) {
var mw = menu.offsetWidth, mh = menu.offsetHeight;
var vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
var px = Math.max(GAP, Math.min(x, vw - mw - GAP));
var py = Math.max(GAP, Math.min(y, vh - mh - GAP));
menu.style.left = px.toFixed(0) + 'px';
menu.style.top = py.toFixed(0) + 'px';
}
function open(x, y) { positionAt(x, y); menu.classList.add('is-open'); }
function close() { menu.classList.remove('is-open'); }
var pressStart = 0, rafId = null, pressX = 0, pressY = 0;
// 계산 겹: (경과시간 / HOLD_MS) 를 0~1 로 clamp 한 진행률을 원둘레(CIRC)에 곱해
// stroke-dashoffset 값을 매 프레임 다시 계산한다 — 시간이 곧 링이 채워지는 양이 된다.
function tickPress(ts) {
var elapsed = performance.now() - pressStart;
var prog = Math.max(0, Math.min(1, elapsed / HOLD_MS));
ringFg.setAttribute('stroke-dashoffset', (CIRC * (1 - prog)).toFixed(1));
if (prog >= 1) { finishPress(); return; }
rafId = requestAnimationFrame(tickPress);
}
function startPress(x, y) {
pressX = x; pressY = y;
pressStart = performance.now();
ring.classList.add('is-active');
ringFg.setAttribute('stroke-dashoffset', CIRC.toFixed(1));
rafId = requestAnimationFrame(tickPress);
}
function cancelPress() {
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
ring.classList.remove('is-active');
ringFg.setAttribute('stroke-dashoffset', CIRC.toFixed(1));
}
function finishPress() {
cancelPress();
open(pressX, pressY);
}
field.addEventListener('pointerdown', function (e) {
root.classList.remove('is-demo'); stopAutoplay();
startPress(e.clientX, e.clientY);
});
field.addEventListener('pointerup', cancelPress);
field.addEventListener('pointerleave', cancelPress);
field.addEventListener('contextmenu', function (e) { e.preventDefault(); });
document.addEventListener('pointerdown', function (e) {
if (menu.classList.contains('is-open') && !menu.contains(e.target) && e.target !== field) close();
});
menu.addEventListener('click', close);
// 자동 루프: 필드 중앙을 누른 채 HOLD_MS 만큼 링이 차오르는 것을 흉내내고, 다 차면 메뉴를 연 뒤
// 잠시 후 닫는다.
var phase = 0, autoTimer = null, simStart = 0;
function autoTick() {
var r = field.getBoundingClientRect();
var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
if (phase === 0) { simStart = performance.now(); pressStart = simStart; pressX = cx; pressY = cy; ring.classList.add('is-active'); phase = 1; }
else if (phase === 1) {
var elapsed = performance.now() - simStart;
var prog = Math.max(0, Math.min(1, elapsed / HOLD_MS));
ringFg.setAttribute('stroke-dashoffset', (CIRC * (1 - prog)).toFixed(1));
if (prog >= 1) { ring.classList.remove('is-active'); open(cx, cy); phase = 2; }
} else if (phase === 2) {
phase = 3;
} else { close(); ringFg.setAttribute('stroke-dashoffset', CIRC.toFixed(1)); phase = 0; }
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } if (rafId) { cancelAnimationFrame(rafId); rafId = null; } }
if (!reduce) { autoTimer = setInterval(autoTick, 130); autoTick(); }
})();import { useEffect, useRef } from "react";
import styles from "./LongPressMobileOpen.module.scss";
const ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 40v128h-48V88H88V40Z" opacity=".2"/><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></g></svg>';
const ICON_PASTE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="M208 48v168a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h40a39.83 39.83 0 0 0-8 24v8h80v-8a39.83 39.83 0 0 0-8-24h40a8 8 0 0 1 8 8" opacity=".2"/><path d="M200 32h-36.26a47.92 47.92 0 0 0-71.48 0H56a16 16 0 0 0-16 16v168a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m-72 0a32 32 0 0 1 32 32H96a32 32 0 0 1 32-32m72 184H56V48h26.75A47.9 47.9 0 0 0 80 64v8a8 8 0 0 0 8 8h80a8 8 0 0 0 8-8v-8a47.9 47.9 0 0 0-2.75-16H200Z"/></g></svg>';
const ICON_RENAME = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="m221.66 90.34l-56 56l-40-40l56-56l29.66-29.66a8 8 0 0 1 11.31 0L221.66 79a8 8 0 0 1 0 11.34" opacity=".2"/><path d="m227.31 73.37l-44.68-44.69a16 16 0 0 0-22.63 0L36.69 152A15.86 15.86 0 0 0 32 163.31V208a16 16 0 0 0 16 16h44.69a15.86 15.86 0 0 0 11.31-4.69L227.31 96a16 16 0 0 0 0-22.63M92.69 208H48v-44.69l88-88L180.69 120ZM192 108.68L147.31 64l24-24L216 84.68Z"/></g></svg>';
const ICON_TRASH = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><g fill="currentColor"><path d="M200 56v152a8 8 0 0 1-8 8H64a8 8 0 0 1-8-8V56Z" opacity=".2"/><path d="M216 48h-40v-8a24 24 0 0 0-24-24h-48a24 24 0 0 0-24 24v8H40a8 8 0 0 0 0 16h8v144a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16V64h8a8 8 0 0 0 0-16M96 40a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v8H96Zm96 168H64V64h128Zm-80-104v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0m48 0v64a8 8 0 0 1-16 0v-64a8 8 0 0 1 16 0"/></g></svg>';
export type LongPressMobileOpenProps = {
color?: string;
};
export default function LongPressMobileOpen({
color = "#2f6df6",
}: LongPressMobileOpenProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const field = root.querySelector<HTMLDivElement>(`.${styles.field}`);
const menu = root.querySelector<HTMLDivElement>(`.${styles.menu}`);
const ring = root.querySelector<SVGSVGElement>(`.${styles.ring}`);
const ringFg = root.querySelector<SVGCircleElement>(`.${styles.ringFg}`);
if (!field || !menu || !ring || !ringFg) return;
const GAP = 6, HOLD_MS = 620, CIRC = 100.5;
function positionAt(x: number, y: number) {
const mw = menu!.offsetWidth, mh = menu!.offsetHeight;
const vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight;
const px = Math.max(GAP, Math.min(x, vw - mw - GAP));
const py = Math.max(GAP, Math.min(y, vh - mh - GAP));
menu!.style.left = px.toFixed(0) + "px";
menu!.style.top = py.toFixed(0) + "px";
}
function open(x: number, y: number) { positionAt(x, y); menu!.classList.add(styles.open); }
function close() { menu!.classList.remove(styles.open); }
let pressStart = 0, rafId: number | null = null, pressX = 0, pressY = 0;
// 계산 겹: (경과시간 / HOLD_MS) 를 0~1 로 clamp 한 진행률을 원둘레(CIRC)에 곱해
// stroke-dashoffset 값을 매 프레임 다시 계산한다 — 시간이 곧 링이 채워지는 양이 된다.
function tickPress() {
const elapsed = performance.now() - pressStart;
const prog = Math.max(0, Math.min(1, elapsed / HOLD_MS));
ringFg!.setAttribute("stroke-dashoffset", (CIRC * (1 - prog)).toFixed(1));
if (prog >= 1) { finishPress(); return; }
rafId = requestAnimationFrame(tickPress);
}
function startPress(x: number, y: number) {
pressX = x; pressY = y;
pressStart = performance.now();
ring!.classList.add(styles.ringOn);
ringFg!.setAttribute("stroke-dashoffset", CIRC.toFixed(1));
rafId = requestAnimationFrame(tickPress);
}
function cancelPress() {
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
ring!.classList.remove(styles.ringOn);
ringFg!.setAttribute("stroke-dashoffset", CIRC.toFixed(1));
}
function finishPress() {
cancelPress();
open(pressX, pressY);
}
function onPointerDown(e: PointerEvent) { stopAutoplay(); startPress(e.clientX, e.clientY); }
function onContextMenu(e: MouseEvent) { e.preventDefault(); }
function onDocDown(e: PointerEvent) {
if (menu!.classList.contains(styles.open) && !menu!.contains(e.target as Node) && e.target !== field) close();
}
field.addEventListener("pointerdown", onPointerDown);
field.addEventListener("pointerup", cancelPress);
field.addEventListener("pointerleave", cancelPress);
field.addEventListener("contextmenu", onContextMenu);
document.addEventListener("pointerdown", onDocDown);
menu.addEventListener("click", close);
// 자동 루프: 필드 중앙을 누른 채 HOLD_MS 만큼 링이 차오르는 것을 흉내내고, 다 차면 메뉴를 연 뒤
// 잠시 후 닫는다.
let phase = 0, autoTimer: number | null = null, simStart = 0;
function autoTick() {
const r = field!.getBoundingClientRect();
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
if (phase === 0) { simStart = performance.now(); pressStart = simStart; pressX = cx; pressY = cy; ring!.classList.add(styles.ringOn); phase = 1; }
else if (phase === 1) {
const elapsed = performance.now() - simStart;
const prog = Math.max(0, Math.min(1, elapsed / HOLD_MS));
ringFg!.setAttribute("stroke-dashoffset", (CIRC * (1 - prog)).toFixed(1));
if (prog >= 1) { ring!.classList.remove(styles.ringOn); open(cx, cy); phase = 2; }
} else if (phase === 2) {
phase = 3;
} else { close(); ringFg!.setAttribute("stroke-dashoffset", CIRC.toFixed(1)); phase = 0; }
}
function stopAutoplay() { if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; } if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } }
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(autoTick, 130); autoTick(); }
return () => {
stopAutoplay();
field.removeEventListener("pointerdown", onPointerDown);
field.removeEventListener("pointerup", cancelPress);
field.removeEventListener("pointerleave", cancelPress);
field.removeEventListener("contextmenu", onContextMenu);
document.removeEventListener("pointerdown", onDocDown);
};
}, []);
return (
<div ref={rootRef} className={styles.stage}>
<div className={styles.cm}>
<div className={styles.field}>
<span className={styles.hint}>길게 누르기</span>
<svg className={styles.ring} viewBox="0 0 40 40">
<circle className={styles.ringBg} cx={20} cy={20} r={16} />
<circle className={styles.ringFg} cx={20} cy={20} r={16} strokeDasharray="100.5" strokeDashoffset="100.5" />
</svg>
</div>
<div className={styles.menu} role="menu">
<button type="button" className={styles.item} role="menuitem">
<span className={styles.ic} style={{ color }} dangerouslySetInnerHTML={{ __html: ICON_COPY }} />
<span>복사</span>
</button>
<button type="button" className={styles.item} role="menuitem">
<span className={styles.ic} style={{ color }} dangerouslySetInnerHTML={{ __html: ICON_PASTE }} />
<span>붙여넣기</span>
</button>
<button type="button" className={styles.item} role="menuitem">
<span className={styles.ic} style={{ color }} dangerouslySetInnerHTML={{ __html: ICON_RENAME }} />
<span>이름 바꾸기</span>
</button>
<button type="button" className={`${styles.item} ${styles.danger}`} role="menuitem">
<span className={styles.ic} dangerouslySetInnerHTML={{ __html: ICON_TRASH }} />
<span>삭제</span>
</button>
</div>
</div>
</div>
);
}09 Long-press reveal — the menu appears once the pressed duration value passes a threshold — Measured area 19.026% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · Context Menu UI: 9 Right-Click Menu Patterns
The post this demo lives in
Context Menu UI: 9 Right-Click Menu PatternsOne menu position value is set from the clicked coordinate value, one flips its direction value near a screen edge, and See all demos in this post
If you like this effect - similar demos
Transitions06 Long-press reveal toggle — once a press-duration value passes a threshold, that spot's expand-amount value grows and extra spots appear above itCSS Bottom Navigation Bar: 9 Transition Patterns
Transitions08 Circle reveal from the icon9 CSS Hamburger Menu Animations, No Library
Transitions09 Scroll-position and shadow values linked — once the scroll-position value crosses a set threshold, the header pins in place and its shadow value deepensCollapsible Table Row: 9 UI Patterns