28 CSS Close Buttons13 / 28
Rotate Out Close Button
The workhorse hover spin, done right: the × rotates a clean 180° on hover, GPU-composited, centred by CSS grid so it can never drift a subpixel mid-spin. This is the demo to learn the three rules every rotating icon needs — grid centring, transform-only motion, and will-change discipline.
Published Updated
The code
<section class="ccb-13" aria-label="Rotating close button demo">
<button class="ccb-13__close" type="button" aria-label="Close">
<span class="ccb-13__x" aria-hidden="true"></span>
</button>
</section><section class="ccb-13" aria-label="Rotating close button demo">
<button class="ccb-13__close" type="button" aria-label="Close">
<span class="ccb-13__x" aria-hidden="true"></span>
</button>
</section>.ccb-13,
.ccb-13 *,
.ccb-13 *::before,
.ccb-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-13 {
--ink: oklch(0.35 0.03 265);
--accent: oklch(0.55 0.18 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.97 0.006 265),oklch(0.93 0.014 265));
}
.ccb-13 .ccb-13__close {
display: grid;
place-items: center;
width: 60px;
height: 60px;
border: 2px solid color-mix(in oklab,var(--ink) 30%,transparent);
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: border-color .25s ease,transform .2s ease;
box-shadow: 0 12px 28px -16px oklch(0.4 0.06 265 / .5);
}
.ccb-13 .ccb-13__x {
position: relative;
width: 22px;
height: 22px;
transition: transform .45s cubic-bezier(.22,1,.36,1);
}
.ccb-13 .ccb-13__x::before,
.ccb-13 .ccb-13__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 2.5px;
border-radius: 2px;
background: var(--ink);
transition: background .25s ease;
}
.ccb-13 .ccb-13__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-13 .ccb-13__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
@media (hover: hover) {
.ccb-13 .ccb-13__x {
will-change: transform;
}
.ccb-13 .ccb-13__close:hover .ccb-13__x {
transform: rotate(180deg);
}
.ccb-13 .ccb-13__close:hover {
border-color: var(--accent);
}
.ccb-13 .ccb-13__close:hover .ccb-13__x::before,
.ccb-13 .ccb-13__close:hover .ccb-13__x::after {
background: var(--accent);
}
}
.ccb-13 .ccb-13__close:active {
transform: scale(.92);
}
.ccb-13 .ccb-13__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.ccb-13 .ccb-13__x,
.ccb-13 .ccb-13__close {
transition: none;
}
}.ccb-13,
.ccb-13 *,
.ccb-13 *::before,
.ccb-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-13 {
--ink: oklch(0.35 0.03 265);
--accent: oklch(0.55 0.18 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.97 0.006 265),oklch(0.93 0.014 265));
}
.ccb-13 .ccb-13__close {
display: grid;
place-items: center;
width: 60px;
height: 60px;
border: 2px solid color-mix(in oklab,var(--ink) 30%,transparent);
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: border-color .25s ease,transform .2s ease;
box-shadow: 0 12px 28px -16px oklch(0.4 0.06 265 / .5);
}
.ccb-13 .ccb-13__x {
position: relative;
width: 22px;
height: 22px;
transition: transform .45s cubic-bezier(.22,1,.36,1);
}
.ccb-13 .ccb-13__x::before,
.ccb-13 .ccb-13__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 2.5px;
border-radius: 2px;
background: var(--ink);
transition: background .25s ease;
}
.ccb-13 .ccb-13__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-13 .ccb-13__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
@media (hover: hover) {
.ccb-13 .ccb-13__x {
will-change: transform;
}
.ccb-13 .ccb-13__close:hover .ccb-13__x {
transform: rotate(180deg);
}
.ccb-13 .ccb-13__close:hover {
border-color: var(--accent);
}
.ccb-13 .ccb-13__close:hover .ccb-13__x::before,
.ccb-13 .ccb-13__close:hover .ccb-13__x::after {
background: var(--accent);
}
}
.ccb-13 .ccb-13__close:active {
transform: scale(.92);
}
.ccb-13 .ccb-13__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.ccb-13 .ccb-13__x,
.ccb-13 .ccb-13__close {
transition: none;
}
}/* No JavaScript — grid centring kills subpixel wobble; transform-only rotation composites on the GPU; will-change is hover-gated. */
/* No JavaScript — grid centring kills subpixel wobble; transform-only rotation composites on the GPU; will-change is hover-gated. */Here's a working CSS Close Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Rotate Out Close Button
Source: https://codefronts.com/components/css-close-buttons/rotate-out/
The workhorse hover spin, done right: the × rotates a clean 180° on hover, GPU-composited, centred by CSS grid so it can never drift a subpixel mid-spin. This is the demo to learn the three rules every rotating icon needs — grid centring, transform-only motion, and will-change discipline.
## HTML
```html
<section class="ccb-13" aria-label="Rotating close button demo">
<button class="ccb-13__close" type="button" aria-label="Close">
<span class="ccb-13__x" aria-hidden="true"></span>
</button>
</section>
```
## CSS
```css
.ccb-13,
.ccb-13 *,
.ccb-13 *::before,
.ccb-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-13 {
--ink: oklch(0.35 0.03 265);
--accent: oklch(0.55 0.18 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.97 0.006 265),oklch(0.93 0.014 265));
}
.ccb-13 .ccb-13__close {
display: grid;
place-items: center;
width: 60px;
height: 60px;
border: 2px solid color-mix(in oklab,var(--ink) 30%,transparent);
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: border-color .25s ease,transform .2s ease;
box-shadow: 0 12px 28px -16px oklch(0.4 0.06 265 / .5);
}
.ccb-13 .ccb-13__x {
position: relative;
width: 22px;
height: 22px;
transition: transform .45s cubic-bezier(.22,1,.36,1);
}
.ccb-13 .ccb-13__x::before,
.ccb-13 .ccb-13__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 2.5px;
border-radius: 2px;
background: var(--ink);
transition: background .25s ease;
}
.ccb-13 .ccb-13__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-13 .ccb-13__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
@media (hover: hover) {
.ccb-13 .ccb-13__x {
will-change: transform;
}
.ccb-13 .ccb-13__close:hover .ccb-13__x {
transform: rotate(180deg);
}
.ccb-13 .ccb-13__close:hover {
border-color: var(--accent);
}
.ccb-13 .ccb-13__close:hover .ccb-13__x::before,
.ccb-13 .ccb-13__close:hover .ccb-13__x::after {
background: var(--accent);
}
}
.ccb-13 .ccb-13__close:active {
transform: scale(.92);
}
.ccb-13 .ccb-13__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.ccb-13 .ccb-13__x,
.ccb-13 .ccb-13__close {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — grid centring kills subpixel wobble; transform-only rotation composites on the GPU; will-change is hover-gated. */
```Here's a working CSS Close Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Rotate Out Close Button
Source: https://codefronts.com/components/css-close-buttons/rotate-out/
The workhorse hover spin, done right: the × rotates a clean 180° on hover, GPU-composited, centred by CSS grid so it can never drift a subpixel mid-spin. This is the demo to learn the three rules every rotating icon needs — grid centring, transform-only motion, and will-change discipline.
## HTML
```html
<section class="ccb-13" aria-label="Rotating close button demo">
<button class="ccb-13__close" type="button" aria-label="Close">
<span class="ccb-13__x" aria-hidden="true"></span>
</button>
</section>
```
## CSS
```css
.ccb-13,
.ccb-13 *,
.ccb-13 *::before,
.ccb-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-13 {
--ink: oklch(0.35 0.03 265);
--accent: oklch(0.55 0.18 265);
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.97 0.006 265),oklch(0.93 0.014 265));
}
.ccb-13 .ccb-13__close {
display: grid;
place-items: center;
width: 60px;
height: 60px;
border: 2px solid color-mix(in oklab,var(--ink) 30%,transparent);
border-radius: 50%;
background: #fff;
cursor: pointer;
transition: border-color .25s ease,transform .2s ease;
box-shadow: 0 12px 28px -16px oklch(0.4 0.06 265 / .5);
}
.ccb-13 .ccb-13__x {
position: relative;
width: 22px;
height: 22px;
transition: transform .45s cubic-bezier(.22,1,.36,1);
}
.ccb-13 .ccb-13__x::before,
.ccb-13 .ccb-13__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 22px;
height: 2.5px;
border-radius: 2px;
background: var(--ink);
transition: background .25s ease;
}
.ccb-13 .ccb-13__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-13 .ccb-13__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
@media (hover: hover) {
.ccb-13 .ccb-13__x {
will-change: transform;
}
.ccb-13 .ccb-13__close:hover .ccb-13__x {
transform: rotate(180deg);
}
.ccb-13 .ccb-13__close:hover {
border-color: var(--accent);
}
.ccb-13 .ccb-13__close:hover .ccb-13__x::before,
.ccb-13 .ccb-13__close:hover .ccb-13__x::after {
background: var(--accent);
}
}
.ccb-13 .ccb-13__close:active {
transform: scale(.92);
}
.ccb-13 .ccb-13__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
.ccb-13 .ccb-13__x,
.ccb-13 .ccb-13__close {
transition: none;
}
}
```
## JavaScript
```js
/* No JavaScript — grid centring kills subpixel wobble; transform-only rotation composites on the GPU; will-change is hover-gated. */
```How this works
Rule one: the button is display:grid; place-items:center and the icon is an explicitly-sized span — grid centring is immune to the half-pixel wobble that plagues top:50%; margin tricks when rotation kicks in. Rule two: the only animated property is transform:rotate(180deg), composited on the GPU with zero paint. Rule three: will-change:transform is applied on :hover-capable contexts only via @media (hover:hover) — promoting the layer permanently on every touch device wastes memory for an effect they can't trigger.
The circle's border brightens with a color transition in parallel, and :active compounds a scale(.92) WITH the rotation — transforms in one property, so they merge instead of overriding.
Make it yours
- Spin 90° for a subtler cue, 360° for exuberance — same single value.
- Chain
rotate() scale()in the hover transform for a grow-spin combo. - Ease with a spring bezier (see demo 17) when the brand is playful.
- Invert: rotate the BUTTON and keep the icon still for a full-tile spin.
Gotchas — read before shipping
- Never rotate an element centred with margin/negative-offset hacks — subpixel drift is visible at 2× displays; grid centring fixes it structurally.
- Don't leave
will-changeon unconditionally; every promoted layer costs compositor memory. - Rotating exactly 45° would turn the × into a + (a plus/add affordance!) — half-rotations of an × must land on 90° multiples.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Everything here is baseline CSS; the lesson is technique, not syntax.