Chonky Menu Re-Creation

This post contains 11 interactive illustrations for you to play with. The final menu is at the bottom. Don’t feel bad if you just want to scroll through fiddling with the demos, or watch the video!

I came across this fun menu design concept shared on Twitter by Janum Trivedi. Go watch it, I’ll wait. Janum added lots of fun details to his menu prototype built in Swift, and I just had to try re-creating it using Javascript (technically Imba).

I don’t usually like to copy other people’s work. I hoped to add some value by putting together these interactive illustrations which show how various parts of the UI work.

Putting Something on the Page

First I set up a menu, and calculated the vertical center of each row. The centers are visualized as horizontal black lines in the illustration below. An event listener tracks mouse movements in order to get the mouse Y position, which is visualized as a red line in the illustration.

Move your mouse (mobile users: press and hold briefly, then drag) over the illustration to see a visualization of the mouse’s Y position.

Now that we have the mouse Y position, and each row’s center Y position, we can easily check which row is closest to the mouse by looping through the list of row positions and comparing them to the mouse position. I’ll store a reference to the closest row, and how far the mouse is from it, visualized in purple below.

Move your mouse over the illustration to see which mark is closest and how far it is from the mouse.

Zoom & Cursor

The original design had a white highlight behind the active menu item, which I’m calling the “cursor”. With the closest row figured out, we can add a box with its Y position such that it is centered on the closest row.

The closest row also zooms in slightly, so I’ve added a CSS scale transform to the closest row. The cursor and zoom effect are visible in the following illustration.

Notice how the cursor (visualized as a green box) jumps to the row closest to the mouse, and how that row scales up slightly.

Decay into Radness

So far, the movements are jumpy. We’ll be adding animations shortly. Lets first add what I’m calling “decay” to the cursor and menu content. In Janum’s original menu, the cursor and menu content gravitate towards the pointer (finger) as it slides past. To accomplish this, I’m using a math function that will translate the distance from the pointer to the closest row, into a decayed value that has the desired effect.

To do the decay, we need a function that for small inputs, returns a value close to that input, and as the input value gets larger, the output starts to slow its rate of increase, smoothly approaching a maximum value which it never passes.

I wasn’t sure what type of math function would work for this, but I had happened to see a Retweet from Matt Sephton of a post by @_phocom which is a convenient reference of math functions that are useful in graphics and game programming.

Looking through these graphs, we have a clear winner: tanh(x). It starts off at Y=0, increasing first quickly, then slowing down and never passing Y=1. The maximum value can be adjusted by multiplying the function by a constant, something like, tanh(x) * max, and the “slowness” of the initial decay can be controlled by manipulating the X value, I ended up with this function:

function decay(x, max, slowness) {
  Math.tanh(x / (max * slowness)) * max;
}

Which creates this graph:

Interactive graph of tanh(x), the hyperbolic tangent function.

The following illustration helps visualize this. You can drag the rectangles and they move together. The blue one follows your pointer and the red one has its Y position run through the decay function. The position of each box is plotted, and you’ll see the blue one traces out a Y=X graph while the red one traces a version of our tanh(x) function.

Drag the red and blue boxes to see the decay effect. The blue box will move with your pointer and the red box has its movement passed through the decay function causing it to smoothly stop at the maximum of 50. Notice how it traces out the tanh(x) function’s graph.

To add this into the menu, I took the distance of the cursor to the closest menu item, passed it through this decay function and used that to calculate the new Y value of the cursor. I used a version with a slightly adjusted maximum for the menu content, so they have different movement, resulting in a subtle parallax.

Move your mouse across the illustration below to see the original value (purple) and decayed values (green for the cursor’s center and blue for the menu content’s center).

Notice how the decayed values slow their movement as the cursor moves away from the center, so they feel like they are trying to reach your cursor but are held back. (The values are exaggerated slightly in this illustration).

Just add Springs

So far there’s no animations. To make this menu feel natural, we’re going to animate it using a spring simulation. I tried using an off-the-shelf animation library but ran into lots of glitches, so I went looking for some spring animation code I could use. I remembered that Cheng Lou had shared his impressive website which is full of springy animations, and the source code is available on GitHub.

I found a simple spring animation function within the code, and (with his permission) I gave it a try. I created a component in my project called SpringLayer which accepts a set of values representing the x, y, width, height, opacity, and scale of the layer. By updating any of those values, the layer will animate to the new values using a spring simulation.

You can play with a SpringLayer in the illustration below. The green dot shows the current “destination” of the rectangle, any time that changes, the layer animates to the new positon. The same applies to all the animated values (like width and height).

Drag, click the arrow buttons, or use the a/s/d/f keys, to move the blue square. Notice how you can interrupt the movement and the animation continues to look smooth. Disable the spring to use a CSS transition instead, and notice that interrupting the animation is less smooth.

The great thing about a spring simulation is that you can interrupt it and give it a new target value, and it will smoothy animate there. Because my animations will be updated every time the mouse moves, this is very important. The spring animations also give a pleasing, lifelike appearance.

So, I recklessly replaced all the elements of my menu that needed to animate with SpringLayers and just set the whole thing to re-render sixty times a second. I thought surely this would cause performance issues because I’d be re-calculating a spring value for each property of each animated layer each frame, but I never had an issue (on desktop at least), so given that this is a prototype, I just went with it.

Adding springs everywhere ended up being easy way to make the animation work.

The same menu from before with animated SpringLayer components inserted in place of div tags.

Activator

The menu is supposed to start out hidden, so I set its opacity to 0% and its scale to 25%, and added an activator button. When pressing on that button, I set the opacity and scale to 100%. Remeber these are “destination” values for the SpringLayer, so the springs go to work and animate the menu to those values.

The cursor doesn’t appear until you first slide your mouse into the menu. You can see a wentIn value in the data display in the illustration below which tracks whether the mouse has crossed over the Y position of the bottom of the menu since the last activation.

The remaining demos require you to press and hold the activator circle to activate the menu.

Press down and hold on the circle to activate the menu, then slide up to reveal the cursor.

Stretch Effect

The coolest part of the original menu is the stretch effect that happens when you drag past the top or bottom of the menu. I can add this by setting a CSS transform value like transform: scaleY(1.2), but that will stretch out from the center, in both directions. The menu needs to stretch only in the direction of the pointer, so I to dynamically update the origin (controlled by the transform-origin css property)

In the next illustration, you’ll see the origin visualized with a red dot. The distance past the top or bottom of the menu are visualized in green.

I also added a way to cancel the menu activation by dragging even further past the ends, which you can try in the illustration below.

Try dragging past the top or bottom to see the stretch effect. Notice how the origin affects the stretch.

I don’t want the scale to continue forever as your mouse continues past the edge, so I run the distance past the edge through our same decay function from before, giving me a value that slows down as it approaches a maximum distance of my choice. Now I have a distance value that approaches a maximum, and I need to translate that into a stretch amount that approaches a maximum stretch. I do the conversion using a range function. This post by Trys Mudford explains range, and other “lerp” functions in Javascript.

Putting it all Together

To finish the menu, I added the names, avatar images, and other polish to make it look like a real menu.

The final menu re-creation.

It was fun building this menu, and I learned a ton. I’m excited to make more fluid UI concepts like this using what I’ve learned.

All credit for the menu design goes to Janum, please follow him on Twitter, he shares really cool prototypes.

Watch the video version of this post