YouTube Music

I use YouTube Music a lot. Around the middle of 2026, it rolled out a redesigned media controls screen. While the reactions were mixed, I actually liked it.

The design engineer in me loved it.

The interface and the interaction design makes it a great case study on how complex interfaces come together.

At first glance, it feels complex to build, with several elements moving and changing at once. The interactions are rich with several swipe gestures and actions. However, if you look closely enough, it's surprisingly simple.

Three States

When I started to study the interface, a few things stood out to me:

  1. The interactions make the interface feel continuous. Despite having various moving elements, it doesn't feel abrupt or janky.
  2. The interface has three distinct states.
  3. The central element of the whole interface and the interaction is, obviously, the album art.

Continuity makes the interface well-composed and the album art becomes the central anchor across the three distinct states.

Let’s look at the three states and how the album art moves across them. In the default state1, the album art sits above the media controls as a large square, inset from the edges of the screen. When the interface is “pushed up,” it enters the peek state2: the album art expands to fill the width of the screen while the controls move up and the queue comes into view. Push it up further into the expanded state3, and the album art shrinks into a small thumbnail in the mini player above the fully open queue.

Default
Peek
Expanded
All three states, in one pull
The player at rest: large album art, title, actions, scrubber and transport controls.

a big square sleeve. padded in from the edges

1Default state
The sheet mid-pull: the album art stretches behind the controls while the queue rises.

full-bleed now. edge to edge, corners gone

2Peek state
The queue open: the album art has shrunk to a thumbnail in a mini bar above the list.

small thumbnail in the mini bar

3Expanded state

Album art

Looking at the three states, one thing is clear: the album art never leaves the screen. It grows, shrinks and moves, but it is always there. So we can't unmount or remove the element from the screen.

We use an absolutely positioned element that sits outside the normal flow of the layout, and control its position and size with four numbers:

  • x: how far it is from the left
  • y: how far it is from the top
  • size: how big it is. It's a square, so one number covers both sides
  • radius: how round its corners are

Each state is just a different set of those four numbers.

So the real question is where these numbers come from.

Setting up the numbers

Before working out the numbers for each state, let's set a few constants. Every state is built from these:

All of them are in logical pixels. TOP_ROW_H is the bar of controls above the album in the default state1, and MINI_ALBUM is the small thumbnail the album art collapses into in the expanded state3.

Swipe through to see how each state's position and size fall out of these.

1Default state
x = PAD= 16
y = insets.top + GAP + TOP_ROW_H + GAP= 118
size = screenW − 2 × PAD= 358
radius = 16
2Peek state
x = 0
y = 0
size = screenW= 390
radius = 0
3Expanded state
x = PAD= 16
y = insets.top + GAP= 62
size = MINI_ALBUM= 40
radius = 6

Rather than scattering these numbers through the component, I work them out once, in a hook. It reads the insets and the screen width, and returns all four numbers for every state:

Then it is one line at the top of the player, const positions = useAlbumPositions();, and every state's layout is there to read. If the insets or the screen width change, say on rotation, the positions change with them.

That gives us three sets of numbers, but the album still needs to know which set to use. So the player keeps one more value, state, that says where it is: 0 for default, 1 for peek, 2 for expanded.

The album turns state into its four numbers by interpolating: at 0 it takes the default values, at 1 the peek values, at 2 the expanded ones:

When put together, we can see how the number changes though the state.

The album moving between its three states

Between the states

Until now, we have been looking at the 3 states as checkpoints and how the album sits at each one. But we talked about how the whole interface feels continuous and interruptible.

This is where state being a number pays off. Earlier we made it 0, 1 and 2 rather than "default", "peek" and "expanded". A label can only ever be one of three things, so the album could only jump between them, or play a canned transition from one to the next.

A number can sit anywhere in between, and interpolation already knows what to do with it: at 0.5 the album is exactly halfway between default and peek, because the same interpolate function calculates the four numbers for any value of state.

Try it with the slider below. It moves state the way a finger on the sheet eventually will.

state = 0.00
DefaultPeekExpanded
Drag along the track. Stop halfway and come back; let go and it settles to the nearest state — and you can catch it again on the way.

Controlling the state

A slider is fine for a demo, but there isn't one in the real player. What people actually drag is the music queue, up from the bottom of the screen.

So we need something on screen in place of the slider to control the progress of the state, and a bottom sheet is a natural fit.

We don't build the sheet ourselves. A bottom sheet already handles the hard parts, like flings, springs and a list that scrolls inside the sheet.

What we give it is a snap point per state: a height the sheet rests at. In the default state it's barely there, just the handle and the queue pill peeking up from the bottom. In peek it rises to about 40% of the screen, enough to show the start of the queue. And when expanded it goes almost all the way up, stopping just below the mini bar.

1Default state
snap = HANDLE_H + PILL_H + insets.bottom= 98
2Peek state
snap = 40% × screenH= 338
3Expanded state
snap = screenH − TOP_BAR_H= 725

The sheet also tracks its own progress. As it moves it reports where it is as animatedIndex: 0 at the first snap point, 1 at the second, 2 at the third, and every value in between. That's the same number our slider was producing, so we pass state straight in:

album.tsx doesn't change. Drag the sheet below and watch the album's numbers follow it.

It moves on its own until you grab it. Drag the sheet up from the bottom: as its height changes, the album's x, y and size follow.

The rest of the screen

The album isn't the only thing that moves. Everything else on the screen follows the same pull, and it's easier to look at them one at a time. Let's start at the top.

The top bar

The top bar is the row we sized as TOP_ROW_H, just below the safe area. It holds a chevron to close the player, the audio/video toggle, cast and a menu.

In the default state it simply sits above the album. Moving to peek, it doesn't move at all. The album grows to full width and slides up underneath it, so the icons end up sitting on top of the cover art.

The change comes in the expanded state. The top bar makes way for the mini bar: the album as a small thumbnail, the song's title and artist, cast and a play button. The mini bar takes the exact same spot the top bar was in, so the top of the screen never jumps.

state
Peek 12 Expanded
1.00
top row opacity 1 → 0
1.00
mini bar opacity 0 → 1
0.00
mini bar y −20 → 0
-20px

That swap only happens between peek and expanded, so both of them read only the second half of state, from 1 to 2:

Between 0 and 1, neither of them does anything. The top bar stays put while the rest of the player rearranges itself around it, which is a big part of why the screen feels anchored.

The queue pill

At the bottom of the player, under the playback controls, there's a small handle and a pill that says "Your queue". It's the only hint that there's a sheet down there at all, and it's what you grab to pull it up.

As the sheet rises towards peek, the pill doesn't ride up with it and stay a pill. It fades and folds away, and in its place the queue's real header fades in: "Playing from Your queue", a Save button and a row of filter chips. The label turns into the header of the thing it was labelling.

state
Default 01 Peek
0.00
pill opacity 1 → 0
1.00
pill height 44 → 0
44px
header opacity 0 → 1
0.00

Both happen in the first half of the pull, from 0 to 1. By the time the sheet reaches peek the pill is gone and the header is fully there, and from peek to expanded neither of them changes:

The pill collapses its height as well as fading. If it only faded, it would leave a 44px gap above the header. Folding it away lets the header move up into the space the pill had, so the two read as one thing changing rather than one leaving and another arriving.

The controls

Between the album and the sheet sits everything you'd use to actually play music: the song's title, a row of action chips (like, lyrics, comments), the progress bar and the playback buttons. They don't all move at once, each one picks its own slice of state.

From default to peek, the action chips fold away, and the rest of the group lifts until its bottom edge sits right on top of the sheet. That's what makes room for the queue: the controls don't get covered, they move up out of its way.

From peek to expanded, as the queue takes over the screen, the group fades out.

state
Default 02 ExpandedPeek 1
0.00
chips height 36 → 0
36px
group y 0 → -190
0px
group opacity 1 → 0
1.00

The chips fold their height and their gap along with their opacity. If they only faded, they'd leave a hole between the title and the progress bar; folding them closes it, so the rest of the group moves up as one.

lift is the one number here that isn't a nice round value. It's however far the group has to rise for its bottom edge to land on the sheet's top edge at peek, so it's worked out from the sheet's snap point and the group's height with the chips folded away.

Final touches

There's one small problem left, and it's easy to miss. Put the real app next to what we've built so far, and watch the second half of the pull, from peek to expanded.

In the app, the controls are gone almost as soon as the sheet starts to move past peek. In ours, they fade out across the whole of it, from 1 to 2. So halfway up they're still half there, sitting on top of the queue that's sliding in underneath them, long after they've stopped being useful.

The fade isn't the problem. The timing is. The fix is to give the fade a much smaller slice of state: instead of spreading it across the whole second half, it gets the first fifth of it, from 1 to 1.2. It's a one-number change:

The interpolation is still a straight line, it's just a much steeper one. Now even a small push of the sheet takes the controls out completely, the same way the app does:

state
Default 02 ExpandedPeek 1
0.00
chips height 36 → 0
36px
group y 0 → -190
0px
group opacity 1 → 0
1.00

And that's really the same idea as everything else on this screen. Every element reads the same state and picks its own slice of it. The last touch is choosing that slice by when an element stops being useful, rather than by where the state boundaries happen to be.

The sheet's surface

There's one more detail that's easy to look straight past: the sheet doesn't look like a sheet for most of the pull.

In the default state and at peek it has no background colour and no shadow. The queue's rows just sit on the player's own background, so the sheet sits flush with the screen. You wouldn't know there's a separate surface there at all, only a list that happens to start below the controls.

It's only on the way to expanded that it starts to look like a sheet. Its background shifts to a slightly different shade from the player behind it, and a soft shadow appears along its top edge. It's a small change, but it's enough to say "this is a layer on top now", right at the point where the queue takes over the screen.

state
Default 02 ExpandedPeek 1
0.00
sheet bg #320C09 0 → 1
0.00
screen wash opacity 1 → 0
1.00
shadow opacity 0 → .25
0.00

Like everything else, it's a slice of state. The background and the shadow both read only the second half of the pull, so they're invisible until peek and fully there by expanded:

Keeping it flush until then is what lets the player and the queue read as one screen at peek, rather than a player with something stacked on top of it.