모션·인터랙션 / Cadence
Pill Menu
내비게이션 · 버튼 · 마이크로 인터랙션 모션. 미리보기와 원본 코드·프롬프트를 이곳에서 확인하세요.
미리보기 안에서 스크롤하거나 마우스를 움직여 모션을 확인하세요.
HTML · CSS · JavaScript 전체 코드
<!doctype html>
<!--
Pill Menu
A frosted-glass pill navigation. On load the pill settles in (6px drop, glass and text
fade in). Clicking the burger morphs it into an X and drops the menu down with a short
fade, slide (-6px → 0) and scale (0.98 → 1). Closes on a link click, a click outside,
or Escape.
Note: browsers can't blur the backdrop of an element (or its parent) while its opacity
is below 1, so the glass never fades with `opacity`: its blur, tint, border and shadow
animate from zero instead, and only the text inside fades.
Dependencies: none (CSS transitions + a few lines of JS).
Tweak (in the CSS):
- Glass: --pill-bg rgba(0,0,0,0.42), blur(18px) saturate(140%), --pill-border
- Load-in: 800ms cubic-bezier(0.2, 0.7, 0.2, 1) slide, 700ms ease-out glass/text fade, 150ms delay
- Menu open: 180ms ease glass/links, 220ms cubic-bezier(0.2, 0.7, 0.2, 1) slide + scale
- Burger → X: bars 220ms ease (±5.5px, ±45°); the middle bar collapses in 120ms / fades in 90ms
From the Paul Hamilton portfolio (own project).
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pill Menu</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
background: #fff;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
a { text-decoration: none; }
/* A photo behind the nav so the frosted glass reads */
.hero {
position: relative;
height: 100vh;
height: 100svh;
overflow: hidden;
background: #e6e6e6;
}
.hero img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Navbar: centred at the top, settles in on load */
.pm-navbar {
--pill-bg: rgba(0, 0, 0, 0.42);
--pill-border: rgba(255, 255, 255, 0.08);
--fg: #f2f2f2;
position: fixed;
top: 18px;
left: 50%;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
transform: translate(-50%, -6px);
transition: transform 800ms cubic-bezier(0.2, 0.7, 0.2, 1) 150ms;
pointer-events: none; /* children re-enable */
}
.pm-navbar.is-loaded { transform: translate(-50%, 0); }
.pm-pill {
display: inline-flex;
align-items: center;
gap: 18px;
padding: 10px 14px 10px 20px;
border: 1px solid transparent;
border-radius: 999px;
background: rgba(0, 0, 0, 0);
-webkit-backdrop-filter: blur(0px) saturate(100%);
backdrop-filter: blur(0px) saturate(100%);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0);
color: var(--fg);
font-size: 14px;
letter-spacing: 0.02em;
pointer-events: auto;
transition:
background-color 700ms ease-out 150ms,
border-color 700ms ease-out 150ms,
box-shadow 700ms ease-out 150ms,
-webkit-backdrop-filter 700ms ease-out 150ms,
backdrop-filter 700ms ease-out 150ms;
}
.pm-pill > * {
opacity: 0;
transition: opacity 700ms ease-out 150ms;
}
.pm-navbar.is-loaded .pm-pill {
border-color: var(--pill-border);
background: var(--pill-bg);
-webkit-backdrop-filter: blur(18px) saturate(140%);
backdrop-filter: blur(18px) saturate(140%);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
}
.pm-navbar.is-loaded .pm-pill > * { opacity: 1; }
.pm-brand { font-weight: 500; white-space: nowrap; }
/* Burger → X */
.pm-burger {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
width: 32px;
height: 32px;
padding: 6px;
border: 0;
border-radius: 999px;
background: transparent;
cursor: pointer;
}
.pm-burger:hover { background: rgba(255, 255, 255, 0.06); }
.pm-burger:focus-visible { outline: 1px solid var(--fg); outline-offset: 2px; }
.pm-burger span {
display: block;
width: 16px;
height: 1.5px;
border-radius: 2px;
background: var(--fg);
transition: transform 220ms ease, opacity 90ms ease;
}
.pm-navbar.is-open .pm-burger span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
/* The middle bar collapses quickly, so it's gone before the X forms */
.pm-navbar.is-open .pm-burger span:nth-child(2) {
opacity: 0;
transform: scaleX(0);
transition: transform 120ms ease, opacity 90ms ease;
}
.pm-navbar.is-open .pm-burger span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }
/* Dropdown */
.pm-menu {
display: flex;
flex-direction: column;
min-width: 200px;
margin-top: 10px;
padding: 8px;
border: 1px solid transparent;
border-radius: 18px;
background: rgba(0, 0, 0, 0);
-webkit-backdrop-filter: blur(0px) saturate(100%);
backdrop-filter: blur(0px) saturate(100%);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0);
transform: translateY(-6px) scale(0.98);
transition:
background-color 180ms ease,
border-color 180ms ease,
box-shadow 180ms ease,
-webkit-backdrop-filter 180ms ease,
backdrop-filter 180ms ease,
transform 220ms cubic-bezier(0.2, 0.7, 0.2, 1);
pointer-events: none;
}
.pm-navbar.is-open .pm-menu {
border-color: var(--pill-border);
background: var(--pill-bg);
-webkit-backdrop-filter: blur(18px) saturate(140%);
backdrop-filter: blur(18px) saturate(140%);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
transform: translateY(0) scale(1);
pointer-events: auto;
}
.pm-menu a {
opacity: 0;
display: block;
padding: 10px 14px;
border-radius: 12px;
color: var(--fg);
font-size: 14px;
letter-spacing: 0.01em;
transition: background 140ms ease, opacity 180ms ease;
}
.pm-navbar.is-open .pm-menu a { opacity: 1; }
.pm-menu a:hover,
.pm-menu a:focus-visible {
background: rgba(255, 255, 255, 0.07);
outline: none;
}
@media (max-width: 540px) {
.pm-pill { gap: 12px; padding: 9px 12px 9px 16px; font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
.pm-navbar,
.pm-pill,
.pm-pill > *,
.pm-menu,
.pm-menu a,
.pm-burger span {
transition-duration: 1ms !important;
transition-delay: 0ms !important;
}
}
</style>
</head>
<body>
<nav class="pm-navbar" id="navbar">
<div class="pm-pill">
<span class="pm-brand">Atelier</span>
<button class="pm-burger" id="burger" aria-label="Open menu" aria-expanded="false" aria-controls="menu">
<span></span><span></span><span></span>
</button>
</div>
<div class="pm-menu" id="menu" aria-hidden="true">
<a href="#about">About</a>
<a href="#services">Services</a>
<a href="#journal">Journal</a>
<a href="#portfolio">Portfolio</a>
<a href="#contact">Contact</a>
</div>
</nav>
<main class="hero">
<!-- Swap the picsum.photos placeholder for your own photo. -->
<img src="https://picsum.photos/seed/cadence-1/1600/1067" alt="">
</main>
<script>
const navbar = document.getElementById("navbar");
const burger = document.getElementById("burger");
const menu = document.getElementById("menu");
function setMenuOpen(open) {
navbar.classList.toggle("is-open", open);
burger.setAttribute("aria-expanded", String(open));
burger.setAttribute("aria-label", open ? "Close menu" : "Open menu");
menu.setAttribute("aria-hidden", String(!open));
}
burger.addEventListener("click", (e) => {
e.stopPropagation();
setMenuOpen(!navbar.classList.contains("is-open"));
});
menu.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
e.preventDefault(); // demo links go nowhere: remove this line on a real site
setMenuOpen(false);
}
});
document.addEventListener("pointerdown", (e) => {
if (navbar.classList.contains("is-open") && !navbar.contains(e.target)) setMenuOpen(false);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") setMenuOpen(false);
});
// The navbar settles in once the page has loaded.
requestAnimationFrame(() => requestAnimationFrame(() => navbar.classList.add("is-loaded")));
</script>
<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/vce701823e78c4301a35b7e07294d87301792088814092" integrity="sha512-UKNk8GbxkABxKcXYuLkO16EleLGUkcvJWtpSNmExE0Too3ZaFAxTscNWEc1OdAv5/BikHawapSCaiD2XCmp+Bw==" data-cf-beacon='{"version":"2024.11.0","token":"c54fa41b83884ade97f239ecb41aee46","r":1,"spa":2}' crossorigin="anonymous"></script>
</body>
</html>
원본 구현 프롬프트
# Pill Menu: frosted-glass pill navigation with a burger that morphs into an X
**Goal:** build a small, centred frosted-glass navigation pill (brand name + burger) that settles in on page load; clicking the burger morphs it into an X and drops a matching glass menu down with a quick fade, slide and scale.
## Visual behaviour
1. **Layout.** A navbar centred at the top of a hero photo (18px from the top). It holds a pill with the brand name on the left and a round burger button on the right, and below it (10px gap) a dropdown panel with five links stacked vertically. The pill and the panel are dark frosted glass with light text (`#f2f2f2`), so they need a photo or colourful content behind them.
2. **Load-in** (starts two animation frames after load, by adding an `is-loaded` class):
- The navbar slides from 6px above its place to its place: 800 ms, `cubic-bezier(0.2, 0.7, 0.2, 1)`, 150 ms delay.
- The pill's glass appears: background from transparent to `rgba(0,0,0,0.42)`, backdrop blur from `blur(0px) saturate(100%)` to `blur(18px) saturate(140%)`, border from transparent to `rgba(255,255,255,0.08)`, shadow from transparent to `0 8px 30px rgba(0,0,0,0.35)`. All 700 ms ease-out, 150 ms delay.
- The brand and burger fade in (opacity 0 → 1), 700 ms ease-out, 150 ms delay.
3. **Burger → X** (when the menu opens, by adding an `is-open` class):
- Three bars, 16 × 1.5px, 4px apart, rounded.
- Top bar: `translateY(5.5px) rotate(45deg)`. Bottom bar: `translateY(-5.5px) rotate(-45deg)`. Both 220 ms ease.
- Middle bar: `scaleX(0)` in 120 ms ease and opacity 0 in 90 ms ease, so it's gone before the X forms.
- Closing reverses it (the outer bars use 220 ms, the middle bar comes back with the base 220 ms transform / 90 ms opacity).
4. **Dropdown open:**
- Panel glass appears the same way as the pill (transparent → `rgba(0,0,0,0.42)`, blur 0 → 18px with saturate 140%, border, shadow `0 16px 40px rgba(0,0,0,0.45)`), 180 ms ease.
- Panel moves from `translateY(-6px) scale(0.98)` to `translateY(0) scale(1)`, 220 ms `cubic-bezier(0.2, 0.7, 0.2, 1)`.
- Links fade from opacity 0 to 1, 180 ms ease.
- While closed the panel has `pointer-events: none`; open, it takes clicks.
- Closing plays the same transitions in reverse.
5. **Link hover/focus:** a faint highlight `rgba(255,255,255,0.07)` behind the link (rounded 12px), 140 ms ease.
6. **Closing:** clicking the burger again, clicking a link, a pointerdown anywhere outside the navbar, or pressing Escape.
7. **Important glass detail:** never fade the glass elements with `opacity`. Browsers switch `backdrop-filter` off on an element whose own opacity (or an ancestor's) is below 1, so the frost would snap in at the end of the fade. Animate the blur, tint, border and shadow from zero instead, and fade only the text inside.
## Exact values
| What | Value |
|---|---|
| Navbar position | centred, 18px from the top |
| Pill | padding `10px 14px 10px 20px`, 18px gap, fully rounded, 14px text, letter spacing 0.02em, brand weight 500 |
| Glass | `rgba(0,0,0,0.42)`, `backdrop-filter: blur(18px) saturate(140%)`, 1px border `rgba(255,255,255,0.08)` |
| Pill shadow | `0 8px 30px rgba(0,0,0,0.35)` |
| Load-in | slide 6px → 0, 800 ms `cubic-bezier(0.2, 0.7, 0.2, 1)`; glass + text 700 ms ease-out; 150 ms delay on all |
| Burger | 32 × 32px round button, 6px padding, hover background `rgba(255,255,255,0.06)` |
| Bars | 16 × 1.5px, 4px gap; X at ±5.5px and ±45°, 220 ms ease; middle bar 120 ms scaleX, 90 ms opacity |
| Panel | min width 200px, padding 8px, radius 18px, 10px below the pill, shadow `0 16px 40px rgba(0,0,0,0.45)` |
| Panel open | glass 180 ms ease; `translateY(-6px) scale(0.98)` → none in 220 ms `cubic-bezier(0.2, 0.7, 0.2, 1)` |
| Links | padding `10px 14px`, radius 12px, 14px, letter spacing 0.01em; opacity 180 ms; hover `rgba(255,255,255,0.07)` 140 ms |
## Markup and structure
```html
<nav class="navbar" id="navbar">
<div class="pill">
<span class="brand">Atelier</span>
<button class="burger" aria-label="Open menu" aria-expanded="false" aria-controls="menu">
<span></span><span></span><span></span>
</button>
</div>
<div class="menu" id="menu" aria-hidden="true">
<a href="#about">About</a>
<a href="#services">Services</a>
…
</div>
</nav>
```
- Navbar: `position: absolute` (or `fixed`), `left: 50%`, `transform: translate(-50%, -6px)` → `translate(-50%, 0)` when loaded; a flex column, items centred; `pointer-events: none` on the navbar itself with the pill and the open panel re-enabling them, so the empty area around it doesn't block the page.
- All states are classes on the navbar: `is-loaded` (load-in) and `is-open` (menu). Everything else is CSS transitions.
- JS: toggle `is-open` on burger click (stop the click from bubbling), update `aria-expanded`, the burger's `aria-label` ("Open menu" / "Close menu") and the panel's `aria-hidden`; close on link click, outside pointerdown and Escape.
## Libraries
- None: CSS transitions and a few lines of plain JS.
## Responsive and reduced motion
- Below 540px: pill gap 12px, padding `9px 12px 9px 16px`, 13px text.
- `prefers-reduced-motion: reduce`: all these transitions take 1 ms with no delay, so the states change instantly.
## Acceptance criteria
- On load the pill drops 6px into place while its frosted glass and text fade in smoothly, with no frost snapping in at the end.
- Clicking the burger turns it into an X (the middle bar disappears first) and the menu appears just below the pill in about 0.2 s with a slight drop and scale-up.
- The photo behind the pill and the panel is visibly blurred and the glass looks the same on both.
- Hovering a link shows a faint rounded highlight.
- The menu closes on the X, on a link, on a click outside and on Escape, reversing the animation, and the X turns back into a burger.
- `aria-expanded`, `aria-label` and `aria-hidden` always match the menu's state.
보관·활용 메모
Cadence에서 제공하는 코드와 프롬프트, 미리보기 영상을 이 사이트에 보관했습니다. 코드는 다운로드한 원본으로 제공하며 실행 화면은 별도로 격리되어 있습니다. 코드의 외부 라이브러리·샘플 이미지 요청은 유지됩니다.
보관일: 2026-10-26 · 프로젝트 적용: 별도 검증 필요
원본 코드·프롬프트·제작자 정보를 함께 보관했습니다. 실제 납품 시 코드와 포함 소재의 이용 조건을 확인하세요.
코드 SHA-256: ea019a35a3ad7ab98c8f195e42837e05a74a2507a47bab4cabedcb8e455946e2