37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
37° 48' 15.7068'' N, 122° 16' 15.9996'' W
cloud-native gis has arrived
Maps
Engineering
Creating UI delight: dynamically rotating mouse cursors
Felt is constantly searching for ways to enhance the user experience & create a sense of delight for our users. Take a look at what that means for the humble cursor.
Felt is constantly searching for ways to enhance the user experience & create a sense of delight for our users. Take a look at what that means for the humble cursor.

In this post, we will explore how to create dynamically rotating mouse cursors for resize and rotate actions, adding a touch of personality and fun to these common interactions. Not only do these custom cursors add an element of surprise and delight, they also improve the usability of your interface by providing a clear visual indication of the current action.

Why affordances are important

One small but impactful issue with graphical applications is that the default cursors for rotating and resizing might not align correctly with a rotated UI element. Cursors are an important affordance in apps like Felt, and when they're not accurate it can create confusion about what the user can expect the app to do.

In this case, the cursor suggests that the user should move the cursor in a specific direction to resize the element.

However, when the element is rotated, just using the standard resize cursors give us a problem. In the next image, it looks like we should move the mouse in a northeast or southwest direction to resize, but that's not really the case.

Fixing this means we need to dynamically render the mouse cursor to reflect the current orientation of the element.

How not to do it: rendering a custom element that tracks the mouse

Let's start just by rendering a static graphic for our cursor, ignoring rotating for now.

We can fake a dynamic cursor by having a fixed-positioned React element that tracks the position of the mouse using state. This is very easy to achieve, and we can easily render whatever we want using the full power of React to that element. When we come to rotating the graphic, it's easy — we can use whatever technique we want such as CSS transforms.

There is one big problem though: the mouse cursor lags behind the "true" mouse position by one frame. This is subtle, but makes the app feel laggy and unresponsive.

This is unavoidable using this technique, so we need to find another way.

Snappy cursors with CSS

Setting the cursor with CSS is straightforward, and we can use SVGs to render any graphic we like.

ℹ️ Here is a great resource for SVG cursors that match the native macOS cursors: https://mac-cursors.netlify.app/.

This gives us cursors that feel more snappy, but we lose some control over what we can render: we cannot render any arbitrary HTML/SVG as we could before in the previous example.

We could opt to generate a static SVG for every rotation of every cursor that we will need, but that's a bit painful. How many should we generate? One for every degree of rotation? Every five degrees? If we want to change the cursor, we need to regenerate all these images. Ideally, we would have something more dynamic.

Base64 to the rescue!

The solution is simple: we can dynamically generate the SVG, render it to a string, base64 encode it, and set that as the cursor style.

Now we can use some basic string templating to get the effect we want. You'll need to get your SVG contents as a string in order to be able to template it, which you can do however you want. At Felt, we have a number of cursors that respond to rotation so we actually end up splitting out a generic system for rotating and drop-shadowing any SVG content we pass to it.

The result is a mouse cursor that is responsive, and dynamically rotates with our element!

You might notice that just doesn't quite look right…with how the shadows look.

This is because the shadows are rotating with the cursor, making it seem like the light source is moving around, leaving us in an uncanny valley of things just feeling a bit "off."

To maintain the illusion of a fixed light source, we need to make sure the shadow stays offset at the same, fixed angle, independent of the rotation of the cursor.

Counter-rotating the shadows

Thankfully, the fix for this is quite straightforward: we can dynamically generate the filter that creates the drop shadow, countering the rotation of the element itself.

With a bit of basic trigonometry, we can calculate the offset required to keep the shadows at a constant offset, to maintain the correct direction of the light source creating the shadows.

The shadows in this video are exaggerated to make the effect more obvious.

Our functions for calculating the drop shadow offset look like this:

Avoiding a flickering cursor

Now we've generated our SVG, we need to set the style. This is as simple as setting a cursor style with a URL property.

We also need to decide where we should apply the style, which requires a little bit of thought for the best user experience.

When we're setting cursors for these kinds of interactions, we need to remember the one-frame lag from the How not to do it section.

Let's assume we have a resizing UI with drag handles on the corner of a selection frame. If we just apply the style on the resize handles, we're going to get flickering when we move the cursor quickly. This is because the pointer is ahead of the rendered content by one frame, so it's possible for the pointer to "escape" the resize handle.

To fix this, we need to handle the cursor styling at a global level, which generally means keeping some application state for which mouse cursor should be shown and updating that as necessary. This is a bit more work than just setting it on the active element, but it's definitely worth it.

You also have to remember to unset your global cursor style when necessary, being careful to consider unhappy paths, like right-clicking while dragging, deleting your element, mousing-up out of the window, etc.

Performance considerations

If your SVG is complex, templating it, converting it to base64, and updating the style on every pointer move might be too expensive.

In practice, this hasn't been an issue for us, but it would be trivial to use quantization of rotation angle, and memoization of the output to reduce the number of times we have to apply the style or recalculate the SVG.

Recap

For something that is a small visual detail, there's a lot of complexity and things to consider. This kind of attention to detail is what can take your app from feeling like an "interactive website" to a desktop-quality piece of software, so they're worth paying attention to.

In summary, what we did was:

  1. Obtain an SVG as a string to use as a template;
  2. Calculate and pass values to your SVG template string to rotate the content;
  3. Calculate and pass values to dynamically set drop shadow;
  4. Base64 encode the result;
  5. Set it as the cursor style, but do this globally during the interaction, unsetting it when finished.

This technique isn't limited to rotating cursors. You could apply this to any kind of situation where you want a dynamic cursor and it's important to keep the cursor completely aligned with the mouse to avoid that annoying one-frame lag. You could use dynamic colors, shapes or even animate different parts of the cursor — you're only limited by what you can draw with SVGs! Try Felt for free to see dynamic cursors in action.

Join us

We are we are building for the next generation of map makers. Our team works hard to ensure every interaction with our product is delightful and intuitive. If you want to work with a team of incredible engineers, apply for one of our open roles.

Bio
LinkedIn
More articles

SOC 2 certified & GDPR compliant: Felt's enterprise-grade security

Add maps to Power BI reports & dashboards with Felt

Simplicity meets power: Felt’s H3 spatial index support has arrived

Understanding spatial indexes: H3 explained