A round plus button that turns into a close X and fans three mini action buttons out in an arc, one after another. Each action rides a rotate, translate, rotate back chain, so its icon stays upright.
Also known as: floating button animation css, speed dial button css, fab menu css.
<div class="btn-speed-dial"> <input class="btn-speed-dial-input" type="checkbox" aria-label="Show actions"> <span class="btn-speed-dial-fab" aria-hidden="true"><i></i><i></i></span> <button class="btn-speed-dial-action" type="button" aria-label="Edit" style="--a:-150deg;--i:0"><svg viewBox="0 0 24 24"><path d="M4 20h4L19 9l-4-4L4 16z"/><path d="M13 7l4 4"/></svg></button> <button class="btn-speed-dial-action" type="button" aria-label="Share" style="--a:-90deg;--i:1"><svg viewBox="0 0 24 24"><circle cx="17.5" cy="5.5" r="2.5"/><circle cx="6.5" cy="12" r="2.5"/><circle cx="17.5" cy="18.5" r="2.5"/><path d="M8.7 10.8l6.6-4M8.7 13.2l6.6 4"/></svg></button> <button class="btn-speed-dial-action" type="button" aria-label="Add photo" style="--a:-30deg;--i:2"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2.5"/><circle cx="9" cy="10" r="1.6"/><path d="M21 16l-5-5-8.5 8"/></svg></button> </div>
.btn-speed-dial {
--c: var(--btn-color, #3b82f6);
position: relative;
display: grid;
width: 3.4em;
height: 3.4em;
margin-top: 3em;
font-size: var(--btn-size, 18px);
}
.btn-speed-dial-input {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
border-radius: 50%;
}
.btn-speed-dial-fab {
display: grid;
place-items: center;
border-radius: 50%;
background-color: var(--c);
color: #fff;
box-shadow: 0 0.45em 1em -0.35em var(--c);
transition: background-color 0.3s ease, transform 0.25s cubic-bezier(0.3, 1.6, 0.5, 1);
}
.btn-speed-dial-fab i {
grid-area: 1 / 1;
width: 1.3em;
height: 0.18em;
border-radius: 0.1em;
background: currentColor;
transition: transform 0.45s cubic-bezier(0.3, 1.5, 0.5, 1);
}
.btn-speed-dial-fab i:last-child { transform: rotate(90deg); }
.btn-speed-dial-action {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
display: grid;
place-items: center;
width: 2.5em;
height: 2.5em;
margin: -1.25em;
padding: 0;
border: 0;
border-radius: 50%;
background: color-mix(in srgb, var(--c) 8%, #fff);
color: var(--c);
box-shadow: 0 0.3em 0.7em -0.2em rgba(15, 23, 42, 0.35), inset 0 0 0 1px color-mix(in srgb, var(--c) 20%, transparent);
cursor: pointer;
opacity: 0;
visibility: hidden;
transform: rotate(var(--a)) translate(0) rotate(calc(-1 * var(--a))) scale(0.4);
transition:
transform 0.3s cubic-bezier(0.5, 0, 0.75, 0) calc((2 - var(--i)) * 0.04s),
opacity 0.2s ease calc((2 - var(--i)) * 0.04s),
visibility 0s 0.4s;
}
.btn-speed-dial-action svg {
width: 1.15em;
height: 1.15em;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.btn-speed-dial:active .btn-speed-dial-fab { transform: scale(0.92); }
.btn-speed-dial-input:focus-visible ~ .btn-speed-dial-fab,
.btn-speed-dial-action:focus-visible {
outline: 2px solid var(--c);
outline-offset: 0.2em;
}
.btn-speed-dial-input:checked ~ .btn-speed-dial-fab { background-color: color-mix(in srgb, var(--c) 75%, #0f172a); }
.btn-speed-dial-input:checked ~ .btn-speed-dial-fab i:first-child { transform: rotate(135deg); }
.btn-speed-dial-input:checked ~ .btn-speed-dial-fab i:last-child { transform: rotate(225deg); }
.btn-speed-dial-input:checked ~ .btn-speed-dial-action {
opacity: 1;
visibility: visible;
transform: rotate(var(--a)) translate(3.6em) rotate(calc(-1 * var(--a))) scale(1);
transition:
transform 0.45s cubic-bezier(0.3, 1.5, 0.5, 1) calc(var(--i) * 0.06s),
opacity 0.2s ease calc(var(--i) * 0.06s),
visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
.btn-speed-dial, .btn-speed-dial *, .btn-speed-dial::before, .btn-speed-dial::after, .btn-speed-dial *::before, .btn-speed-dial *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--btn-color on the element or a parent.
Currently previewing #3b82f6.--btn-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 18px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.