Navigation blueprint
Music player hero expansion
A mini player expands around one persistent album cover. The artwork is the hero element linking both states while the title moves and listening details enter.
Inspired by zero (@twoclipping) on X ↗
Build it
Choose a target and adapt the source to your interface.
<div class="hero-player" data-expanded="false">
<div
class="player-art"
role="img"
aria-label="Blue record artwork for Slow Orbit"
></div>
<div class="player-title">
<strong>Slow Orbit</strong><small>Studio North</small>
</div>
<button
class="player-expand"
type="button"
aria-expanded="false"
aria-label="Expand player"
>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 14 6-6 6 6" /></svg>
</button>
<div class="player-details" aria-hidden="true">
<div class="player-progress"><i></i></div>
<div class="player-times"><span>1:24</span><span>3:42</span></div>
<div class="player-caption">Now playing · Slow Orbit</div>
</div>
</div>
<script>
const player = document.querySelector('.hero-player');
const button = player.querySelector('.player-expand');
const details = player.querySelector('.player-details');
function setExpanded(expanded) {
player.dataset.expanded = String(expanded);
button.setAttribute('aria-expanded', String(expanded));
button.setAttribute(
'aria-label',
expanded ? 'Collapse player' : 'Expand player',
);
details.setAttribute('aria-hidden', String(!expanded));
}
button.addEventListener('click', () =>
setExpanded(player.dataset.expanded !== 'true'),
);
player.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setExpanded(false);
});
</script>
<style>
.hero-player,
.hero-player * {
box-sizing: border-box;
}
.hero-player {
position: relative;
width: 280px;
height: 76px;
overflow: hidden;
border: 1px solid var(--stage-grid, var(--line, #cbd0d7));
border-radius: 16px;
background: var(--stage-bg, var(--bg, #fafafa));
color: var(--stage-ink, var(--ink, #252b34));
font: 14px/1.4 system-ui;
transition: height 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-player[data-expanded='true'] {
height: 340px;
}
.player-art {
position: absolute;
left: 14px;
top: 14px;
width: 160px;
height: 160px;
border-radius: 12px;
background: #185bcc;
color: white;
transform-origin: top left;
transform: scale(0.3);
transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1);
display: grid;
place-items: center;
overflow: hidden;
}
.player-art::before {
content: '';
width: 108px;
height: 108px;
border: 18px solid #ffffffc9;
border-radius: 50%;
}
.player-art::after {
content: '';
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
background: white;
}
.hero-player[data-expanded='true'] .player-art {
transform: translate(45px, 10px) scale(1);
}
.player-title {
position: absolute;
top: 17px;
left: 76px;
transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.player-title strong,
.player-title small {
display: block;
}
.player-title small {
color: var(--stage-muted, var(--muted, #596170));
font-size: 12px;
margin-top: 3px;
}
.hero-player[data-expanded='true'] .player-title {
transform: translate(-52px, 183px);
}
.player-expand {
position: absolute;
right: 8px;
top: 15px;
width: 44px;
height: 44px;
border: 0;
background: transparent;
color: inherit;
border-radius: 8px;
cursor: pointer;
}
.player-expand svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-player[data-expanded='true'] .player-expand svg {
transform: rotate(180deg);
}
.player-details {
position: absolute;
top: 258px;
left: 24px;
right: 24px;
opacity: 0;
transform: translateY(12px);
transition:
opacity 180ms ease-out,
transform 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-player[data-expanded='true'] .player-details {
opacity: 1;
transform: none;
transition-delay: 150ms;
}
.player-progress {
height: 3px;
background: var(--stage-grid, var(--line, #cbd0d7));
border-radius: 2px;
}
.player-progress i {
display: block;
width: 38%;
height: 100%;
background: var(--stage-link, var(--brand-deep, #185bcc));
}
.player-times {
display: flex;
justify-content: space-between;
font-size: 11px;
margin-top: 7px;
color: var(--stage-muted, var(--muted, #596170));
font-variant-numeric: tabular-nums;
}
.player-caption {
text-align: center;
font-size: 12px;
margin-top: 10px;
}
.player-expand:hover {
background: color-mix(in srgb, currentColor 8%, transparent);
}
.player-expand:focus-visible {
outline: 2px solid var(--stage-link, var(--brand-deep, #185bcc));
outline-offset: -2px;
}
@media (prefers-reduced-motion: reduce) {
.hero-player,
.hero-player * {
transition: none !important;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
Rest: artwork sits beside the track title in a 76px mini player.
0–150ms: the same shell expands and the artwork moves toward centre.
150–330ms: the progress details fade in beneath the moving title.
330–500ms: the 160px artwork settles into the expanded player. Collapse reverses the geometry and immediately fades the details.
Motion intent
A mini player expands around one persistent album cover. The artwork is the hero element linking both states while the title moves and listening details enter.
500ms makes the relationship between multiple moving elements readable while updating the chosen state immediately.
A quick departure and gradual settle connect both states without bounce. Every spatial element follows the same curve.
Motion specification
- Trigger
- Expand the mini player, then collapse it or press Escape.
- Timing
- 500ms
- Easing
- cubic-bezier(.22, 1, .36, 1)
Animated properties
Implementation notes
- Keep one artwork element mounted through expansion and collapse; animate its position and scale from its top-left anchor.
- The shell intentionally animates height because the component footprint changes. Keep the artwork and title on transforms.
- Delay details by 150ms on expansion; fade them immediately on collapse. Do not apply will-change to text or artwork the preview camera scales.
- This blueprint demonstrates player layout. Connect playback and seeking to your application audio state separately.
Reduced motion
Apply the selected geometry and final content immediately without translation or scaling.
- Use a 44px expand button with aria-expanded and an updated action label.
- Support Enter, Space, and Escape; keep focus on the persistent expand button.
- Hide collapsed details from assistive technology.
Avoid
- Do not replace or crossfade duplicate album covers.
- Do not let the expanding artwork obscure the collapse target.