When I first wrote my hexagon guide[1] in 2013 I used d3.js[2], which has a nice animation system. I had some trouble with CSS transitions in SVG back then, so I was using Javascript transitions using SVG attributes instead of CSS. This looked something like:
d3.select(".grid") .transition() .attr("transform", "rotate(30)");
That would rotate the grid from flat-topped to pointy-topped, but the text would be rotated too:
The solution was to undo the rotation on the <text>
nodes:
d3.select(".grid text") .transition() .attr("transform", "rotate(-30)");
When I rewrote the hexagon guide in Vue in 2018[3], I wrote my own animations that set a Javascript value between 0 and 1, and turned that into an angle between 0° and 30°. I then applied that angle to both the grid and the text:
<svg> <g class="grid" :transform="rotationTransform"> <g v-for="hex in grid"> <text :transform="undoRotationTransform" /> </g> </g> </svg>
In both of these implementations, I used a Javascript value to change SVG attributes. As an optimization, I limited the animation to diagrams that were visible. Diagrams that weren’t visible would “snap” to the final position.
During the rewrite I was hoping to simplify the implementation a little bit.
SVG2 added a feature in 2016 specifically to solve the problem of rotated text: vector-effect: non-rotation
[4]. Caniuse shows it being supported[5], and the MDN page shows it as supported[6]. However when I tested it back in 2018, and again in 2024, it didn’t work anywhere[7]. There’s a Chrome Bug[8] that says the non-rotation
value is unimplemented since 2017, and a Firefox bug[9] that says they won’t implement it unless Chrome does (probably because developers won’t use all the features Firefox has implemented that Chrome didn’t). So I can’t use this feature.
I wanted to use CSS transitions[10] back then, but browser features and bugs back then limited what I could do. Since 2018, browsers have adopted lots of new features, and many bugs have been fixed. I decided to try using CSS transitions again.
But why CSS transitions? Partly because it’s easier to program, and partly because it should be faster. With Javascript transitions, I can start from something like this:
<svg> <g transform="rotate(0)"> <text transform="rotate(0)"> <text transform="rotate(0)"> <text transform="rotate(0)"> <text transform="rotate(0)"> <text transform="rotate(0)"> <text transform="rotate(0)"> … </g> </svg>
and want to turn it into this:
<svg> <g transform="rotate(30)"> <text transform="rotate(-30)"> <text transform="rotate(-30)"> <text transform="rotate(-30)"> <text transform="rotate(-30)"> <text transform="rotate(-30)"> <text transform="rotate(-30)"> … </g> </svg>
During the transition, I have to change all of those numbers every animation frame. As of today, I have 2145 elements ✕ 60frames/sec ✕ 0.5sec = 64,350 html updates to run the full animation. With CSS transitions, I can do this instead:
<svg> <g class="rotate-by-0"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> … </g> </svg>
and then change it to
<svg> <g class="rotate-by-30"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> <text class="unrotate"> … </g> </svg>
just once, not every frame. And I only have to change the class at the top, not all the individual <text>
elements. That means I have 39 html updates to run the full animation … down from 64,350!
First, I needed to make sure CSS transitions would work. I designed a CSS easing function and tested it with an online playground[11]. It worked across the browsers I test on.
I implemented this change in small steps and tested each step across browsers:
- Switch from
transform
attribute totransform
style, still controlled via Javascript. - Refactor styles to have the diagram rotate and the text unrotate.
- Switch from inline
style
toclass
, moving the style rules to the global CSS. - Add CSS transition rule, transition: transform 0.5s; transition-timing-function: cubic-bezier(0.5, -0.2, 0.5, 1.2);.
- Some elements couldn’t use CSS transitions so wrote a Javascript approximation to it, along with a test to make sure the CSS and Javascript versions stay in sync.
I did run into one bug, with Safari this time. The animations should all play when I change the CSS class. In Safari, for elements not currently on the screen, it would start the animation when the element is scrolled into view. That’s too late. It should have finished by then. To work around this bug, I used IntersectionObserver
to trigger the animation only on visible elements.
I’m glad I was able to simplify the implementation. I hope the animation runs smoother on more devices, but on my computers I wasn’t able to tell.