28 CSS Close Buttons12 / 28

Pure CSSMIT licensed

Lock Tumble Close Button

A safe-cracking dismiss: a brass-ringed dial whose × sits inside a notched bezel. Hover triggers a mechanical combination — the cross ratchets left, further left, then snaps right past centre and settles — driven by one steps()-flavoured keyframe sequence, while the bezel's notch markings counter-rotate like a tumbler wheel.

Published Updated

Live Demo
Try it

The code

<section class="ccb-12" aria-label="Lock tumbler close button demo">
  <button class="ccb-12__close" type="button" aria-label="Close">
    <span class="ccb-12__ticks" aria-hidden="true"></span>
    <span class="ccb-12__x" aria-hidden="true"></span>
  </button>
</section>
@property --ccb-12-a {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.ccb-12,
.ccb-12 *,
.ccb-12 *::before,
.ccb-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccb-12 {
  --brass-a: oklch(0.78 0.1 85);
  --brass-b: oklch(0.55 0.09 75);
  --spin: 2.4s;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(120% 120% at 50% 0%,oklch(0.32 0.04 60),oklch(0.2 0.02 50));
}

.ccb-12 .ccb-12__close {
  position: relative;
  width: 84px;
  height: 84px;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  background: conic-gradient(from 210deg,var(--brass-a),var(--brass-b) 30%,var(--brass-a) 52%,var(--brass-b) 78%,var(--brass-a));
  box-shadow: 0 18px 40px -16px rgba(0,0,0,.75),inset 0 2px 3px rgba(255,255,255,.45),inset 0 -3px 6px rgba(0,0,0,.4);
}

.ccb-12 .ccb-12__close::before {
  content: "";
  position: absolute;
  inset: 10px;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%,oklch(0.42 0.05 70),oklch(0.26 0.03 60) 70%);
  box-shadow: inset 0 2px 6px rgba(0,0,0,.6);
}

.ccb-12 .ccb-12__ticks {
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background: repeating-conic-gradient(from var(--ccb-12-a),transparent 0 8deg,oklch(0.3 0.03 60 / .9) 8deg 10deg);
  -webkit-mask: radial-gradient(circle,transparent 62%,#000 63%,#000 74%,transparent 75%);
  mask: radial-gradient(circle,transparent 62%,#000 63%,#000 74%,transparent 75%);
}

.ccb-12 .ccb-12__x {
  position: absolute;
  inset: 0;
  display: block;
}

.ccb-12 .ccb-12__x::before,
.ccb-12 .ccb-12__x::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 3.5px;
  border-radius: 2px;
  background: linear-gradient(180deg,oklch(0.92 0.06 85),oklch(0.7 0.08 80));
  box-shadow: 0 1px 2px rgba(0,0,0,.5);
}

.ccb-12 .ccb-12__x::before {
  transform: translate(-50%,-50%) rotate(45deg);
}

.ccb-12 .ccb-12__x::after {
  transform: translate(-50%,-50%) rotate(-45deg);
}

.ccb-12 .ccb-12__close:hover .ccb-12__x {
  animation: ccb-12-tumble 1.1s cubic-bezier(.3,0,.2,1) 1 both;
}

.ccb-12 .ccb-12__close:hover .ccb-12__ticks {
  animation: ccb-12-ring var(--spin) linear infinite;
}

@keyframes ccb-12-tumble {
  0% {
    transform: rotate(0);
  }

  14%,
    26% {
    transform: rotate(-20deg);
  }

  40%,
    52% {
    transform: rotate(-38deg);
  }

  68%,
    78% {
    transform: rotate(18deg);
  }

  100% {
    transform: rotate(0);
  }
}

@keyframes ccb-12-ring {
  to {
    --ccb-12-a: -360deg;
  }
}

.ccb-12 .ccb-12__close:active {
  transform: scale(.94);
}

.ccb-12 .ccb-12__close:focus-visible {
  outline: 3px solid var(--brass-a);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .ccb-12 .ccb-12__close:hover .ccb-12__x,
    .ccb-12 .ccb-12__close:hover .ccb-12__ticks {
    animation: none;
  }
}
/* No JavaScript — a hold-heavy keyframe ratchets the × like tumbler clicks; @property spins the masked tick ring the other way. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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: Lock Tumble Close Button
Source: https://codefronts.com/components/css-close-buttons/lock-tumble/

A safe-cracking dismiss: a brass-ringed dial whose × sits inside a notched bezel. Hover triggers a mechanical combination — the cross ratchets left, further left, then snaps right past centre and settles — driven by one steps()-flavoured keyframe sequence, while the bezel's notch markings counter-rotate like a tumbler wheel.
## HTML
```html
<section class="ccb-12" aria-label="Lock tumbler close button demo">
  <button class="ccb-12__close" type="button" aria-label="Close">
    <span class="ccb-12__ticks" aria-hidden="true"></span>
    <span class="ccb-12__x" aria-hidden="true"></span>
  </button>
</section>
```
## CSS
```css
@property --ccb-12-a {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.ccb-12,
.ccb-12 *,
.ccb-12 *::before,
.ccb-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccb-12 {
  --brass-a: oklch(0.78 0.1 85);
  --brass-b: oklch(0.55 0.09 75);
  --spin: 2.4s;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(120% 120% at 50% 0%,oklch(0.32 0.04 60),oklch(0.2 0.02 50));
}

.ccb-12 .ccb-12__close {
  position: relative;
  width: 84px;
  height: 84px;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  background: conic-gradient(from 210deg,var(--brass-a),var(--brass-b) 30%,var(--brass-a) 52%,var(--brass-b) 78%,var(--brass-a));
  box-shadow: 0 18px 40px -16px rgba(0,0,0,.75),inset 0 2px 3px rgba(255,255,255,.45),inset 0 -3px 6px rgba(0,0,0,.4);
}

.ccb-12 .ccb-12__close::before {
  content: "";
  position: absolute;
  inset: 10px;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%,oklch(0.42 0.05 70),oklch(0.26 0.03 60) 70%);
  box-shadow: inset 0 2px 6px rgba(0,0,0,.6);
}

.ccb-12 .ccb-12__ticks {
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background: repeating-conic-gradient(from var(--ccb-12-a),transparent 0 8deg,oklch(0.3 0.03 60 / .9) 8deg 10deg);
  -webkit-mask: radial-gradient(circle,transparent 62%,#000 63%,#000 74%,transparent 75%);
  mask: radial-gradient(circle,transparent 62%,#000 63%,#000 74%,transparent 75%);
}

.ccb-12 .ccb-12__x {
  position: absolute;
  inset: 0;
  display: block;
}

.ccb-12 .ccb-12__x::before,
.ccb-12 .ccb-12__x::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 3.5px;
  border-radius: 2px;
  background: linear-gradient(180deg,oklch(0.92 0.06 85),oklch(0.7 0.08 80));
  box-shadow: 0 1px 2px rgba(0,0,0,.5);
}

.ccb-12 .ccb-12__x::before {
  transform: translate(-50%,-50%) rotate(45deg);
}

.ccb-12 .ccb-12__x::after {
  transform: translate(-50%,-50%) rotate(-45deg);
}

.ccb-12 .ccb-12__close:hover .ccb-12__x {
  animation: ccb-12-tumble 1.1s cubic-bezier(.3,0,.2,1) 1 both;
}

.ccb-12 .ccb-12__close:hover .ccb-12__ticks {
  animation: ccb-12-ring var(--spin) linear infinite;
}

@keyframes ccb-12-tumble {
  0% {
    transform: rotate(0);
  }

  14%,
    26% {
    transform: rotate(-20deg);
  }

  40%,
    52% {
    transform: rotate(-38deg);
  }

  68%,
    78% {
    transform: rotate(18deg);
  }

  100% {
    transform: rotate(0);
  }
}

@keyframes ccb-12-ring {
  to {
    --ccb-12-a: -360deg;
  }
}

.ccb-12 .ccb-12__close:active {
  transform: scale(.94);
}

.ccb-12 .ccb-12__close:focus-visible {
  outline: 3px solid var(--brass-a);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .ccb-12 .ccb-12__close:hover .ccb-12__x,
    .ccb-12 .ccb-12__close:hover .ccb-12__ticks {
    animation: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — a hold-heavy keyframe ratchets the × like tumbler clicks; @property spins the masked tick ring the other way. */
```

How this works

The ratchet motion is a single @keyframes with hard stops: percentages hold each angle (−20°, hold, −38°, hold, +18°, settle 0°) with tiny 2% transitions between holds — reading as discrete mechanical clicks rather than a smooth tween. It runs once per hover (animation … 1 both), like dialing a combination.

The bezel's tick marks are a repeating-conic-gradient masked to a thin ring with a radial-gradient mask — zero extra elements — and slowly counter-rotate via an @property --a angle animation while hovered. Brass comes from a conic metal gradient on the outer ring; the inner face is a radial brushed highlight.

Make it yours

  • Re-metal via the conic stops (brass → steel: desaturate to oklch grays).
  • Retune the combination by editing the keyframe angles — odd asymmetric angles read most mechanical.
  • Speed the tick-ring or reverse its direction with --spin.
  • Play the tumble on :active instead of :hover to make the unlock feel earned by the press.

Gotchas — read before shipping

  • The holds need real duration (≥12% of the timeline) — without them the ratchet blurs into a wobble.
  • @property is required for animating the tick-ring angle; without registration the custom property snaps instead of interpolating (falls back gracefully here).
  • Keep the × upright at rest (end angle 0) — a resting tilt reads as broken, not mechanical.

Browser support

ChromeSafariFirefoxEdge
115+17+128+115+

@property drives the counter-rotating ring (static fallback elsewhere); repeating-conic-gradient + mask are 2021 evergreens.

Techniques used in this demo

Search CodeFronts

Loading…