I all the time see this Google Gemini button up within the nook in Gmail. Whenever you hover over it, it does this cool animation the place the little four-pointed star spins and the outer form morphs between a pair completely different shapes which are additionally spinning.
I challenged myself to recreate the button utilizing the brand new CSS form()
operate sprinkled with animation to get issues fairly shut. Let me stroll you thru it.
Drawing the Shapes
Breaking it down, we want 5 shapes in complete:
- 4-pointed star
- Flower-ish factor (sure, that’s the technical time period)
- Cylinder-ish factor (additionally the proper technical time period)
- Rounded hexagon
- Circle
I drew these shapes in a graphics enhancing program (I like Affinity Designer, however any app that allows you to draw vector shapes ought to work), outputted them in SVG, after which used a instrument, like Temani Afif’s generator, to translate the SVG paths this system generated to the CSS form()
syntax.
Now, earlier than I exported the shapes from Affinity Designer, I made certain the flower, hexagon, circle, and cylinder all had the identical variety of anchor factors. In the event that they don’t have the identical quantity, then the shapes will soar from one to the following and gained’t do any morphing. So, let’s use a constant variety of anchor factors in every form — even the circle — and we are able to watch these shapes morph into one another.
I set twelve anchor factors on every form as a result of that was the very best quantity used (the hexagon had two factors close to every curved nook).
One thing associated (and presumably arduous to unravel, relying in your graphics program) is that a few of my shapes had been wildly contorted when animating between shapes. For instance, many shapes turned smaller and started spinning earlier than morphing into the following form, whereas others had been way more seamless. I finally discovered that the interpolation was matching every form’s place to begin and continued matching factors because it adopted the form.
The result’s that the matched factors transfer between shapes, so if the start line for one form is on reverse aspect of the start line of the second form, loads of motion is critical to transition from one form’s place to begin to the following form’s place to begin.

Fortunately, the circle was the one form that gave me hassle, so I used to be capable of spin it (with some trial and error) till its place to begin extra intently matched the opposite beginning factors.
One other concern I bumped into was that the cylinder-ish form had two particular person straight strains in form()
with line
instructions reasonably than utilizing the curve
command. This prevented the animation from morphing into the following form. It instantly snapped to the following picture with out animating the transition, skipping forward to the following form (each when going into the cylinder and popping out of it).
I went again into Affinity Designer and ever-so-slightly added curvature to the 2 strains, after which it morphed completely. I initially thought this was a form()
quirk, however the identical factor occurred after I tried the animation with the path()
operate, suggesting it’s extra an interpolation limitation than it’s a form()
limitation.
As soon as I completed including my form()
values, I outlined a CSS variable for every form. This makes the later makes use of of every form()
extra readable, to not point out simpler to take care of. With twelve strains per form the code is stinkin’ lengthy (technical time period) so we’ve put it behind an accordion menu.
View Form Code
:root {
--hexagon: form(
evenodd from 6.47% 67.001%,
curve by 0% -34.002% with -1.1735% -7.7% / -1.1735% -26.302%,
curve by 7.0415% -12.1965% with 0.7075% -4.641% / 3.3765% -9.2635%,
curve by 29.447% -17.001% with 6.0815% -4.8665% / 22.192% -14.1675%,
curve by 14.083% 0% with 4.3725% -1.708% / 9.7105% -1.708%,
curve by 29.447% 17.001% with 7.255% 2.8335% / 23.3655% 12.1345%,
curve by 7.0415% 12.1965% with 3.665% 2.933% / 6.334% 7.5555%,
curve by 0% 34.002% with 1.1735% 7.7% / 1.1735% 26.302%,
curve by -7.0415% 12.1965% with -0.7075% 4.641% / -3.3765% 9.2635%,
curve by -29.447% 17.001% with -6.0815% 4.8665% / -22.192% 14.1675%,
curve by -14.083% 0% with -4.3725% 1.708% / -9.7105% 1.708%,
curve by -29.447% -17.001% with -7.255% -2.8335% / -23.3655% -12.1345%,
curve by -7.0415% -12.1965% with -3.665% -2.933% / -6.334% -7.5555%,
shut
);
--flower: form(
evenodd from 17.9665% 82.0335%,
curve by -12.349% -32.0335% with -13.239% -5.129% / -18.021% -15.402%,
curve by -0.0275% -22.203% with -3.1825% -9.331% / -3.074% -16.6605%,
curve by 12.3765% -9.8305% with 2.3835% -4.3365% / 6.565% -7.579%,
curve by 32.0335% -12.349% with 5.129% -13.239% / 15.402% -18.021%,
curve by 20.4535% -0.8665% with 8.3805% -2.858% / 15.1465% -3.062%,
curve by 11.58% 13.2155% with 5.225% 2.161% / 9.0355% 6.6475%,
curve by 12.349% 32.0335% with 13.239% 5.129% / 18.021% 15.402%,
curve by 0.5715% 21.1275% with 2.9805% 8.7395% / 3.0745% 15.723%,
curve by -12.9205% 10.906% with -2.26% 4.88% / -6.638% 8.472%,
curve by -32.0335% 12.349% with -5.129% 13.239% / -15.402% 18.021%,
curve by -21.1215% 0.5745% with -8.736% 2.9795% / -15.718% 3.0745%,
curve by -10.912% -12.9235% with -4.883% -2.2595% / -8.477% -6.6385%,
shut
);
--cylinder: form(
evenodd from 10.5845% 59.7305%,
curve by 0% -19.461% with -0.113% -1.7525% / -0.11% -18.14%,
curve by 10.098% -26.213% with 0.837% -10.0375% / 3.821% -19.2625%,
curve by 29.3175% -13.0215% with 7.2175% -7.992% / 17.682% -13.0215%,
curve by 19.5845% 5.185% with 7.1265% 0% / 13.8135% 1.887%,
curve by 9.8595% 7.9775% with 3.7065% 2.1185% / 7.035% 4.8195%,
curve by 9.9715% 26.072% with 6.2015% 6.933% / 9.4345% 16.082%,
curve by 0% 19.461% with 0.074% 1.384% / 0.0745% 17.7715%,
curve by -13.0065% 29.1155% with -0.511% 11.5345% / -5.021% 21.933%,
curve by -26.409% 10.119% with -6.991% 6.288% / -16.254% 10.119%,
curve by -20.945% -5.9995% with -7.6935% 0% / -14.8755% -2.199%,
curve by -8.713% -7.404% with -3.255% -2.0385% / -6.1905% -4.537%,
curve by -9.7575% -25.831% with -6.074% -6.9035% / -9.1205% -15.963%,
shut
);
--star: form(
evenodd from 50% 24.787%,
curve by 7.143% 18.016% with 0% 0% / 2.9725% 13.814%,
curve by 17.882% 7.197% with 4.171% 4.2025% / 17.882% 7.197%,
curve by -17.882% 8.6765% with 0% 0% / -13.711% 4.474%,
curve by -7.143% 16.5365% with -4.1705% 4.202% / -7.143% 16.5365%,
curve by -8.6115% -16.5365% with 0% 0% / -4.441% -12.3345%,
curve by -16.4135% -8.6765% with -4.171% -4.2025% / -16.4135% -8.6765%,
curve by 16.4135% -7.197% with 0% 0% / 12.2425% -2.9945%,
curve by 8.6115% -18.016% with 4.1705% -4.202% / 8.6115% -18.016%,
shut
);
--circle: form(
evenodd from 13.482% 79.505%,
curve by -7.1945% -12.47% with -1.4985% -1.8575% / -6.328% -10.225%,
curve by 0.0985% -33.8965% with -4.1645% -10.7945% / -4.1685% -23.0235%,
curve by 6.9955% -12.101% with 1.72% -4.3825% / 4.0845% -8.458%,
curve by 30.125% -17.119% with 7.339% -9.1825% / 18.4775% -15.5135%,
curve by 13.4165% 0.095% with 4.432% -0.6105% / 8.9505% -0.5855%,
curve by 29.364% 16.9% with 11.6215% 1.77% / 22.102% 7.9015%,
curve by 7.176% 12.4145% with 3.002% 3.7195% / 5.453% 7.968%,
curve by -0.0475% 33.8925% with 4.168% 10.756% / 4.2305% 22.942%,
curve by -7.1135% 12.2825% with -1.74% 4.4535% / -4.1455% 8.592%,
curve by -29.404% 16.9075% with -7.202% 8.954% / -18.019% 15.137%,
curve by -14.19% -0.018% with -4.6635% 0.7255% / -9.4575% 0.7205%,
curve by -29.226% -16.8875% with -11.573% -1.8065% / -21.9955% -7.9235%,
shut
);
}
If all that appears like gobbledygook to you, it largely does to me too (and I wrote the form()
Almanac entry). As I stated above, I transformed them from stuff I drew to form()
s with a instrument. If you happen to can acknowledge the shapes from the customized property names, you then’ll have all it’s worthwhile to know to maintain following alongside.
Breaking Down the Animation
After staring on the Gmail animation for longer than I want to admit, I used to be capable of acknowledge six distinct phases:
First, on hover:
- The four-pointed star spins to the correct and modifications shade.
- The flowery blue form spreads out from beneath the star form.
- The flowery blue form morphs into one other form whereas spinning.
- The purplish shade is wiped throughout the flowery blue form.
Then, after hover:
- The flowery blue form contracts (principally the reverse of Part 2).
- The four-pointed star spins left and returns to its preliminary shade (principally the reverse of Part 1).
That’s the run sheet we’re working with! We’ll write the CSS for all that in a bit, however first I’d wish to arrange the HTML construction that we’re hooking into.
The HTML
I’ve all the time needed to be a kind of front-enders who make jaw-dropping artwork out of CSS, like illustrating the Sistine chapel ceiling with a single div
(cue somebody commenting with a CodePen doing simply that). However, alas, I made a decision I wanted two div
s to perform this problem, and I thanks for wanting previous my disgrace. To these of you who turned up your nostril and stopped studying after that admission: I can safely name you a Flooplegerp and also you’ll by no means understand it.
(To these of you continue to with me, I don’t truly know what a Flooplegerp is. However I’m certain it’s unhealthy.)
As a result of the animation must unfold out the blue form from beneath the star form, they must be two separate shapes. And we are able to’t shrink or clip the primary ingredient to do that as a result of that may obscure the star.
So, yeah, that’s why I’m reaching for a second div
: to deal with the flowery form and the way it wants to maneuver and work together with the star form.
The Fundamental CSS Styling
Every form is basically outlined with the identical field with the identical dimensions and margin spacing.
#geminianimation {
width: 200px;
aspect-ratio: 1/1;
margin: 50px auto;
place: relative;
}
We are able to clip the field to a selected form utilizing a pseudo-element. For instance, let’s clip a star form utilizing the CSS variable (--star
) we outlined for it and set a background shade on it:
#geminianimation {
width: 200px;
aspect-ratio: 1;
margin: 50px auto;
place: relative;
&::earlier than {
content material: "";
clip-path: var(--star);
width: 100%;
top: 100%;
place: absolute;
background-color: #494949;
}
}
We are able to hook into the container’s youngster div
and use it to determine the animation’s beginning form, which is the flower (clipped with our --flower
variable):
#geminianimation div {
width: 100%;
top: 100%;
clip-path: var(--flower);
background: linear-gradient(135deg, #217bfe, #078efb, #ac87eb, #217bfe);
}
What we get is a star form stacked proper on high of a flower form. We’re virtually carried out with our preliminary CSS, however with the intention to recreate the animated shade wipes, we want a a lot bigger floor that permits us to “change” colours by shifting the background gradient’s place. Let’s transfer the gradient in order that it's declared on a pseudo ingredient as an alternative of the kid div
, and dimension it up by 400%
to provide us extra respiration room.
#geminianimation div {
width: 100%;
top: 100%;
clip-path: var(--flower);
&::after {
content material: "";
background: linear-gradient(135deg, #217bfe, #078efb, #ac87eb, #217bfe);
width: 400%;
top: 400%;
place: absolute;
}
}
Now we are able to clearly see how the shapes are positioned relative to one another:
Animating Phases 1 and 6
Now, I’ll admit, in my very own hubris, I’ve turned up my very personal schnoz on the humble transition
property as a result of my considering is usually, Transitions are nice for getting began in animation and for fast issues, however actual animations are carried out with CSS keyframes.
(Maybe I, too, am a Flooplegerp.)
However now I see the error of my methods. I can write a set of keyframes that rotate the star 180 levels, flip its shade white(ish), and have it keep that means for so long as the ingredient is hovered. What I can’t do is animate the star again to what it was when the ingredient is un-hovered.
I can, nonetheless, try this with the transition
property. To do that, we add transition: 1s ease-in-out;
on the ::earlier than
, including the brand new background shade and rotating issues on :hover
over the #geminianimation
container. This accounts for the primary and sixth phases of the animation we outlined earlier.
#geminianimation {
&::earlier than {
/* Present types */
transition: 1s ease-in-out;
}
&:hover {
&::earlier than {
remodel: rotate(180deg);
background-color: #FAFBFE;
}
}
}
Animating Phases 2 and 5
We are able to do one thing comparable for the second and fifth phases of the animation since they're mirror reflections of one another. Keep in mind, in these phases, we’re spreading and contracting the flowery blue form.
We are able to begin by shrinking the interior div
’s scale
to zero initially, then broaden it again to its authentic dimension (scale: 1
) on :hover
(once more utilizing transitions):
#geminianimation {
div {
scale: 0;
transition: 1s ease-in-out;
}
&:hover {
div {
scale: 1;
}
}
Animating Part 3
Now, we very properly may deal with this with a transition
like we did the final two units, however we most likely shouldn't do it… that's, until you wish to weep bitter tears and curse the day you first heard of CSS… not that I do know from private expertise or something… ha ha… ha.
CSS keyframes are a greater match right here as a result of there are a number of states to animate between that may require defining and orchestrating a number of completely different transitions. Keyframes are more proficient at tackling multi-step animations.
What we’re principally doing is animating between completely different shapes that we’ve already outlined as CSS variables that clip the shapes. The browser will deal with interpolating between the shapes, so all we want is to inform CSS which form we would like clipped at every part (or “part”) of this set of keyframes:
@keyframes shapeshift {
0% { clip-path: var(--circle); }
25% { clip-path: var(--flower); }
50% { clip-path: var(--cylinder); }
75% { clip-path: var(--hexagon); }
100% { clip-path: var(--circle); }
}
Sure, we may mix the primary and final keyframes (0%
and 100%
) right into a single step, however we’ll want them separated in a second as a result of we additionally wish to animate the rotation on the similar time. We’ll set the preliminary rotation to 0turn
and the ultimate rotation 1turn
in order that it may possibly hold spinning easily so long as the animation is continuous:
@keyframes shapeshift {
0% {
clip-path: var(--circle);
rotate: 0turn;
}
25% {
clip-path: var(--flower);
}
50% {
clip-path: var(--cylinder);
}
75% {
clip-path: var(--hexagon);
}
100% {
clip-path: var(--circle);
rotate: 1turn;
}
}
Word: Sure, flip
is certainly a CSS unit, albeit one that usually goes missed.
We wish the animation to be easy because it interpolates between shapes. So, I’m setting the animation’s timing operate with ease-in-out
. Sadly, this may also decelerate the rotation because it begins and ends. Nevertheless, as a result of we’re each starting and ending with the circle form, the truth that the rotation slows popping out of 0% and slows once more because it heads into 100% isn't noticeable — a circle seems to be like a circle regardless of its rotation. If we had been ending with a unique form, the easing can be seen and I'd use two separate units of keyframes — one for the shape-shift and one for the rotation — and name them each on the #geminianimation
youngster div
.
#geminianimation:hover {
div {
animation: shapeshift 5s ease-in-out infinite forwards;
}
}
Animating Part 4
That stated, we nonetheless do want yet one more set of keyframes, particularly for altering the form’s shade. Keep in mind how we set a linear gradient on the mum or dad container’s ::after
pseudo, then we additionally elevated the pseudo’s width
and top
? Right here’s that little bit of code once more:
#geminianimation div {
width: 100%;
top: 100%;
clip-path: var(--flower);
&::after {
content material: "";
background: linear-gradient(135deg, #217bfe, #078efb, #ac87eb, #217bfe);
width: 400%;
top: 400%;
place: absolute;
}
}
The gradient is that giant as a result of we’re solely displaying a part of it at a time. And which means we are able to translate
the gradient’s place to maneuver the gradient at sure keyframes. 400%
could be properly divided into quarters, so we are able to transfer the gradient by, say, three-quarters of its dimension. Since its mum or dad, the #geminianimation div
, is already spinning, we don’t want any fancy actions to make it really feel like the colour is coming from completely different instructions. We simply translate
it linearly and the spin provides some variability to what route the colour wipe comes from.
@keyframes gradientMove {
0% {
translate: 0 0;
}
100% {
translate: -75% -75%;
}
}
One ultimate refinement
As a substitute of utilizing the flower because the default form, let’s change it to circle. This smooths issues out when the hover interplay causes the animation to cease and return to its preliminary place.
And there you've it:
Wrapping up
We did it! Is that this precisely how Google completed the identical factor? In all probability not. In all honesty, I by no means inspected the animation code as a result of I needed to strategy it from a clear slate and work out how I'd do it purely in CSS.
That’s the enjoyable factor a couple of problem like this: there are alternative ways to perform the identical factor (or one thing comparable), and your means of doing it's prone to be completely different than mine. It’s enjoyable to see a wide range of approaches.
Which leads me to ask: How would you've approached the Gemini button animation? What concerns would you consider that perhaps I haven’t?