Next.js version 15.2
features a redesigned developer tools experience. I contributed interaction design improvements to the surfaces in collaboration with Pranathi Peri, Jiwon Choi, Jiachi Liu, and many other colleagues.
The Dev Tools Badge comes in two states— collapsed and expanded. The expanded state only appears when there are errors in the application.
Focus rings
Press on the element and use the Tab key to move between elements with the keyboard.
What do you notice about the focus rings?
Tabbing through the elements uncovers several interaction areas. What we call the currently active hit area for keyboard input is a focus ring. It shows the user which element they will activate by pressing the Enter key.
In the error state , we use an inset white color for the focus rings which gives them better contrast against the red than the Next.js branded blue outset ring which on the flip side works better for the default state.
Concentric hit areas look visually more appealing, but also make maximum use of available space for greater pointer Ergonomics
Multiple hit areas makes structuring the markup really important. We are not nesting <button>
elements which would be invalid, but placing them as siblings, and visually increasing target sizes with CSS:
State animations
You might have noticed that we also transition the width of the surface when it morphs into an errorenous state.
If you look away from the element for a moment, you'll notice that it animating in your peripheral vision draws your attention to it.
Motion is a great way to draw attention to an element that offers new, useful information if the element was previously on the screen in another state.
Humans are sensitive to subtle changes in peripheral vision so it's important to communicate only useful, not cosmetic changes unrelated to their primary intent.
Often state transitions accidentally play out when the page loads, but this makes the application feel poorly built.
In the case of a build error where the application is broken before anything loads, the component becomes immediately on the first render.
But it should not animate its width from the state because the immediate transition feels jarring. State transitions are best reserved for responding to input or changes in the system.
The fix for the behavior we need is usually trivial. We use measured values when they are available, otherwise assume the intrinsic size with auto
:
But if the element needs to render immediately and has async content, you should defer using measured values until the dimensions of the element have stabilised 1
You might have noticed that the issue count will also animate when it updates. It works without external libraries which was a requirement for this given project.
Instead, the component uses the key
prop in React to replay CSS keyframes, like in the Logos Carousel prototype.
Reduced responsiveness
Which of these animations do you have an easier time following?
Sometimes updates happen with higher frequency than an animation can run. The component in production would sometimes quickly re-render 2-3 times because it received the error count in chunks as a performance optimization.
In our animation callback, we can measure how much time has passed since each update, and cancel animations to reduce Responsiveness
Debugging states
I want to emphasise how I found this bug in the first place.
There are several states this component can be in, and reproducing them while working on it means a lot of clicking back and forth.
So I made a mini debug tool and assigned the states to keyboard shortcuts to quickly toggle between them:
You can try it out by spamming I to increase the issue count, X to collapse, and R helps to quickly reset state.
What's the takeaway here? Interactions should feel robust, interruptible, and in the worst case—tolerate spamming without breaking. And the only way to sand all edge cases 2 is to just click around a lot until you stop finding broken states.
Notch anatomy
This is the overlay Next.js will launch when your application contains errors. For now, we will be inspecting the navigation items.
The actions are housed in cut-off containers, creating a notch-like effect.
Curving a corner with a custom path in CSS is practically impossible, so we need to be creative here.
The way we structure markup here is important. We want to make sure the contents can grow, so we only render the curved tail as an SVG element that we export from Figma and glue it to a HTML element to create the illusion of a continuous curve.
Our implementation strategy for the notch means it will not break the effect when content inside the tabs grows.
Scroll fading
The header retains its position during scroll for quick access to the navigation.
Unfortunately this leaves our interface feeling visually unappealing because the header ends up clipping content, creating Overlapping layers
In my backpocket I always have a Fade component to pull in for interactions like these:
We can position the fade as sticky
to keep it anchored to the top as we scroll.
But if we leave it as is, it will always be visible and obscure content even when we aren't scrolling.
We want to Interpolate the scroll position to opacity
on the fade. This way the reveal will be smooth and relative to how much you have scrolled:
The 15
is an arbitrary value that you would choose depending on how fast you want the fader to become visible.
If you try and scroll it now, the opacity
will be relative to how much you scroll, and caps out at 1
Drag mechanics
The badge can also be dragged to four different corners of the screen in case it covers any application content
For this, no libraries were used to keep the bundle size to a minimum. Instead, a custom useDrag()
hook powers drag interactions and uses Projection to snap to the nearest corner based on velocity.
By disabling projection can you guess why repositioning the element with a short flick suddenly feels almost impossible?
What is projection and why is it useful? When a drag gesture ends, we can use the speed of the movement to determine where the element would land if it were to continue moving. This is called a projected position.3
If you try and drag again, the projected position is now visually represented with the element
Projection enhances drag gestures to swipe gestures, enabling short flicks to move the element to another position without you having to drag it halfway there.
For further details, I encourage you to view the pull request of the drag mechanics demonstrated here.
Notes
- Design high-contrast and co-centric interaction areas
- Never nest button elements, instead use siblings
- Use animation to draw attention to changes in the system
- Sand edge cases by toggling between states with shortcuts
- Skip animations for high-frequency updates
- Consider how custom SVG shapes will adjust to content resizing
- Soften scroll-clipped contents with a blur fade
- Use projection and velocity based snapping for drag interactions
Takeaways
- Fix error dialog resizing logic
- Jim Nielsen talks about "sanding UI" as a woodworking metaphor for testing user interfaces by "clicking around a ton"
- At 43:00 Nathan de Vries from the Apple Design Team demonstrates the projected position mechanic at WWDC 2018
- Implement notch design
- Blur fader for scrollable container
Resources
Prototypes Next.js Dev Tools