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 ↗

Playground

Playhead 0ms / 500ms

Build it

Choose a target and adapt the source to your interface.

194 lines
index.html
<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.

500ms
01

Rest: artwork sits beside the track title in a 76px mini player.

02

0–150ms: the same shell expands and the artwork moves toward centre.

03

150–330ms: the progress details fade in beneath the moving title.

04

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

heighttransformopacity

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.

Related blueprints

Browse all blueprints