28 Tailwind CSS close buttons for modal and dialog dismiss, drawer and lightbox overlays, cookie and consent banners, toast and ad-banner dismiss, and chat and video-player chrome — spanning corner-X and hamburger-to-X toggles, neumorphic and brutalist and neon styling, hold-to-confirm friction, and playful hover motion (rotate, magnetic, glitch, particle burst, galaxy vortex). Scoped under .tcb-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 28 Tailwind close buttons. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
The canonical dialog dismiss, rebuilt in utilities: a quiet 44px circle pinned with absolute top-3 end-3, its × drawn by two before:/after: bars — no icon font, no SVG. Hover tints the circle with hover:bg-tcb01-acc/12 and stretches the bars with group-hover:before:scale-x-115; the dialog itself fades in with zero JS via starting:.
A close button that passes WCAG 2.4.7 and 2.4.11 on any surface: focus-visible:outline-3 plus a two-stop focus-visible:shadow-[…] halo paints ink-on-white-on-ink, so the ring reads on both the light and dark panels. The border and × are currentColor, so one class list serves both themes, and forced-colors: hands the glyph to the system palette in Windows High Contrast.
Safari's tab-overview dismiss: a 28px translucent systemGray circle with backdrop-blur-md, legible over any page thumbnail. The port fixes the original's biggest flaw — a 28px tap target — by making the button a 44px size-11 hit area and the circle a child span, and shows two tabs side by side so you can judge legibility over light and dark thumbnails.
The support-widget launcher: a gradient FAB whose chat bubble spins out as the × spins in, driven entirely by group-aria-expanded: — the script flips one ARIA attribute and never touches a class. The panel enters and exits on data-open: with a visibility transition, so the original's setTimeout/hidden juggling is gone, and <kbd>Esc</kbd> now closes it.
A GDPR-style consent bar whose Dismiss pill carries a CSS-drawn × after its label. data-dismissed:translate-y-[120%] data-dismissed:opacity-0 data-dismissed:invisible slides it away; inert removes it from the tab order the instant it leaves. The port also fixes the original's dead "Accept all" button — both choices now close the banner and announce what happened.
The IAB rule in one component: a 20px visual dot inflated to a 48×48 tap target with p-3.5 and pulled back into the corner with -mt-3.5 -me-3.5. The original's "set --debug:0" instruction became a real toggle: a checkbox the stage reads with group-has-[:checked]/stage:, so the dashed hit-area overlay switches on and off with zero JavaScript.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Soft UI done responsibly: two opposing shadows — a light one up-left, a dark one down-right — extrude the button from the surface; active: swaps them for inset copies so it physically sinks with zero layout shift. The port adds what neumorphism usually forgets: a 3:1 hairline edge, a solid focus ring, and a glyph that turns accent on press so state is not signalled by shadow alone.
Three bars become an ×: the top bar drops translate-y-2.5 and turns rotate-45, the middle one fades with scale-x-40, the bottom mirrors the top — all keyed off group-aria-expanded:. The menu wipes open with an animated clip-path, and the port fixes a real bug in the original: the collapsed links were still in the tab order. Now the closed nav is inert and <kbd>Esc</kbd> closes it.
Player chrome that gets out of the way: a gradient top bar with a blurred glass × that fades after 2.4s of stillness via group-data-idle/player:opacity-0. The port closes the original's keyboard gap — any key press or focus inside the player now wakes the chrome, and it never idles while focus is inside — and adds a matching bottom scrubber that hides with it.
A slide-over built on <dialog>: the browser traps focus, <kbd>Esc</kbd> closes it and focus returns to the opener for free. Tailwind v4 animates both directions with open:translate-x-0, starting:open:translate-x-full and transition-discrete — the entry and the exit, with the dialog still painted while it slides out. The port turns the demo into a real filter drawer with an Apply footer.
Each bar of the × is a strip of creased paper: perspective-[400px] on the button, transform-3d on the rig, and on hover each pseudo-element tips back along its own length with group-hover:before:rotate-x-[58deg], its two-tone gradient becoming the lit and shaded faces of the fold. Keyboard focus now folds it too.
A brass combination dial: the × ratchets through hold-heavy keyframes like tumbler clicks while a masked tick ring counter-spins, its angle driven by a typed @property. Both motions are @theme animations — --animate-tcb12-tumble and --animate-tcb12-ring — applied with group-hover:animate-tcb12-*. The @property registration is the one thing that must stay CSS.
The most-searched close-button animation, done cleanly: a 180° group-hover:rotate-180 on the glyph with an ease-out-expo curve, the ring and bars shifting to the accent. grid place-items-center kills the sub-pixel wobble that plagues spinning icons, and v4's hover: is already gated behind @media (hover:hover) — the original's hand-written media query simply disappears.
The award-site magnetic button: inside a 170px field the button leans toward the cursor and the × leans further, a parallax that makes the glyph feel attracted. The script writes four custom properties; Tailwind reads them with translate-x-(--tcb14-mx). On leave, group-data-releasing/zone: swaps in an elastic overshoot curve. Because translate and scale are separate properties, active:scale-92 presses without cancelling the pull.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Metaballs in utilities: a blurred, re-thresholded SVG filter applied with [filter:url(#tcb15-goo)] fuses a main blob and two droplets into one liquid shape. Hover (or keyboard focus) flings the droplets out on a back-and-overshoot curve with group-hover/stack:translate-x-[38px], and the surface tension visibly snaps as they separate.
RGB-split glitch on a cyber-terminal button: the × is painted from two gradient bars stored in one arbitrary custom property, and two tinted ghost copies — cyan and magenta via before:text-* and currentColor — jitter on desynced steps() tracks while clip-path: inset() tears them into scanlines. Keyframes live in @theme; the rest is markup.
A little machine: on hover two steel plates slam inward with group-hover:translate-y-[15px] while the × is crushed flat by scale-y-22 and bows out with scale-x-125. One anticipation curve, declared once in @theme as --ease-tcb17-crush, drives every part, so the plates and the glyph move as a single mechanism.
The Material ink ripple without a library: the script records the press point as --tcb18-rx/--tcb18-ry, the after: pseudo-element is positioned there with after:left-(--tcb18-rx), and data-rippling:after:animate-tcb18-ink blooms it. Keyboard presses ripple from the centre, so the feedback is identical for every input.
Eight particles from one 6px dot: a single keyframe animates eight box-shadow copies outward and shrinks them, while the × squashes and pops. The CSS original played it on :active, so a quick tap cut the burst off after a frame or two; the port triggers it with a data-burst attribute that the script clears on animationend, so every tap gets the full 550ms.
A spring with no physics engine: cubic-bezier(.68,-.55,.265,1.55) — declared once as --ease-tcb20-spring — winds back below zero, overshoots past one, and settles, so a plain group-hover:rotate-90 reads as a snap. The drafting-grid face is four stacked backgrounds in a single arbitrary property; press squashes the whole tile on the same spring.
A neon tube that strikes on hover: the × idles as a dim, lightly glowing filament, then a ballast-misfire keyframe flickers it before three stacked drop-shadow() shells lock it at full glow. A blurred after: puddle brightens on the glass below. The port raises the idle state to a legible 3:1 — the original's resting × sat at 35% opacity and nearly vanished.
The dismiss control is the ribbon: a 150px band rotated with rotate-45, parked off the card's corner with -end-[38px] and clipped by overflow-hidden, with a counter-rotated -rotate-45 × that stays upright. Because v4's translate is applied before rotate, hover:-translate-y-[3px] lifts the band straight up without restating the angle.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The × splits along its horizon and both halves hinge away from the viewer like a split-flap display: two copies of one glyph, cropped with [clip-path:inset(0_0_50%_0)] and its mirror, tip with group-hover:rotate-x-[78deg] and -rotate-x-[78deg] while brightness-72 darkens them into shadow. A 40ms stagger makes the fold feel physical.
Destructive-action friction: a conic progress ring fills while you press and hold, completing after 900ms; release early and it drains back. The fill is a typed @property percentage transitioned via data-holding:[--tcb24-p:100%]. The port fixes a silent bug in the original — the property was registered inherits:false, so the ring pseudo-element never saw the value — and announces completion to screen readers.
A success toast with a draining countdown bar that owns the clock: animate-tcb25-life shrinks it over 5s and animationend dismisses the toast — no duplicate timer. Hover or keyboard focus pauses it with group-hover/toast:[animation-play-state:paused], satisfying WCAG 2.2.1 (Timing Adjustable). The × is a 40px padded target pulled into the corner with negative margins.
Legible over any photo: dark translucent fill, backdrop-blur-md and a white hairline — three layers that survive white skies and black shadows alike — with top-[max(14px,env(safe-area-inset-top))] keeping the × clear of the notch in full-screen viewers. The port adds the chrome real lightboxes need: a counter and a scrim-backed caption.
Neo-brutalism with honest physics: a thick black border and a zero-blur shadow-[4px_4px_0_#000]; hover lifts the button with hover:-translate-1 while the shadow grows to 8px, and press slams it down with active:translate-1 active:shadow-none — transform and shadow move in opposition, so the "floor" never moves. Set in Space Grotesk and Space Mono.
A spiral galaxy in a button: two conic-gradient arm layers counter-rotate at co-prime speeds (11s and 7s) via two typed @property angles, a ten-star field is a single box-shadow, and hover whips both arms up to 3.5s/2.2s with group-hover:[animation-duration:3.5s] as the × slowly spins and shrinks into the vortex.
FAQ
Frequently asked questions
How do I pin a close X to the top-right corner of a Tailwind modal?
Demo 01 uses absolute top-3 end-3 on the button, so it anchors to the corner of the nearest positioned ancestor — which means the dialog card itself needs relative, or the X escapes to the page corner. That is the most common bug with this pattern. Use the logical end-3 rather than right-3 so the control mirrors correctly in right-to-left layouts. The glyph is a real icon button, not text, so it carries an aria-label like "Close dialog" and a type="button" attribute to stop it submitting a surrounding form. The 44px circle is a deliberate target size — the visible ring is smaller, but padding inflates the hit area so a thumb never misses. Keep the X above the card's own content in the stacking order with a small z-index if the card scrolls its body.
How do I make a Tailwind close button accessible to keyboard and screen-reader users?
An icon-only button has no accessible name, so every demo here gives it aria-label="Close" and the actual glyph is marked decorative. Demo 02 adds the keyboard half: focus-visible:outline-3 paints a visible ring only when the button is reached by Tab, not on mouse click, so pointer users never see a stray outline while keyboard users always do. Never suppress the ring with outline-none and no replacement — that fails WCAG 2.4.7. The ring uses an offset so it stays visible on both light and dark surfaces. Demo 10 goes further by building the panel on a native <dialog> element: the browser traps focus inside, Esc closes it for free, and focus returns to the trigger on close. If you hand-roll a <div> overlay instead, you inherit all of that focus management yourself.
How does the rotate-on-hover close button work in Tailwind?
Demo 13 wraps the glyph in a parent marked group and spins the inner X with group-hover:rotate-180 plus a transition and a duration. Rotating the parent would also move the focus ring and any label, so only the inner span turns — that separation is the whole trick. Add group-focus-visible:rotate-180 alongside the hover variant so keyboard users get the same feedback; hover-only animation is invisible to anyone not using a pointer. The rotation is purely cosmetic, so it belongs behind a motion-reduce:transition-none or a @media (prefers-reduced-motion: reduce) guard — a spinning X on every hover is exactly the kind of small motion that triggers vestibular discomfort. Because it is a transform, it runs on the compositor and never reflows the button, so it is cheap even on a list of many close buttons.
Why is my mobile close button hard to tap even though it looks fine?
A close X that is visually 16 or 20px is a real accessibility failure on touch — fingers miss it and users rage-tap the content behind it. Demo 06 fixes this the correct way: the visible dot stays a 20px mark, but p-3.5 inflates the total interactive box to a 48×48 tap target, matching the platform minimum. The padding is transparent, so the design still reads as a small dot while the hit area is large. Do not chase the size by scaling the icon up — a giant X looks clumsy and still fails if the surrounding padding is zero. If two dismiss controls sit close together, give each its own 48px box and add a little gap so their targets do not overlap. On banners and ads this is not optional; a mistargeted close is the difference between a dismiss and an accidental click-through.
How do I animate a hamburger menu icon into a close X in Tailwind?
Demo 08 stacks three bars and transforms them on a toggled state. The top bar drops with translate-y-2.5 and rotates rotate-45, the bottom bar rises the same distance and rotates the opposite way, and the middle bar fades to opacity-0 so the two crossing bars read as an X. The bug people hit is the pivot: unless the bars share a transform origin at their common center, the rotation swings them apart instead of crossing. Set the origin to center and match the translate distance to the bar gap so the ends meet cleanly. This control is a toggle, not a plain close, so it needs aria-expanded reflecting the open state and an aria-controls pointing at the menu it operates — a screen reader has to announce "expanded" or "collapsed," which the visual morph alone never conveys. Guard the transition with motion-reduce.
How do I add a hold-to-confirm delay before a destructive close fires?
Demo 24 adds friction to a dangerous dismiss so a stray tap cannot wipe something. A conic-gradient progress ring fills while the pointer is held down and only completes after 900ms; release early and it resets. The mechanism is a CSS animation on the ring bound to the button's active/pressed state, with a script confirming the action only once the fill reaches full. The critical detail is that pointerdown starting the fill must be paired with both pointerup and pointercancel to reset — miss pointercancel and a scroll gesture that steals the pointer leaves the ring stuck mid-fill. For keyboard parity, treat a held Enter or a two-step confirm as the equivalent, because you cannot "press and hold" with a keyboard. Reserve this for genuinely destructive closes; on an ordinary modal it just makes people hold a button for no reason.
Which of these close buttons are pure CSS and which need JavaScript?
Most are pure CSS. Hover, rotate, fold, neon, glitch, spring, ribbon and the galaxy vortex are all driven by transition, @keyframes, :hover and @property — no script runs. The ones that genuinely need JavaScript are the ones reacting to a runtime measurement or a real event. Demo 18's Material ripple records the exact press point into custom properties --tcb18-rx and --tcb18-ry so the ink expands from where you clicked; CSS alone cannot read a click coordinate. Demo 14's magnetic button reads the cursor position inside its field to lean the X toward it. Demo 24's hold-to-confirm needs a timer to fire the action, and Demo 10's native <dialog> uses showModal(). For the CSS-only ones, closing the panel is still your job — the button styles the control, but you wire the actual dismiss to state or to <dialog>.
When should I not use one of the heavily animated close buttons?
The showcase demos — liquid goo, glitch, galaxy vortex, neon strike, particle burst, trash compactor — are fun but they draw the eye and cost attention, so keep them off high-frequency, task-critical dismissals. A cookie banner, an error toast or a payment modal wants the quiet corner X from Demo 01 or the WCAG-clean button from Demo 02, where the goal is "get out of my way instantly." An animation that delays the visible dismiss by a few hundred milliseconds reads as lag. Anything with continuous motion — the vortex, the neon idle glow — must respect prefers-reduced-motion or it becomes an accessibility problem, and a perpetually animating element also keeps the compositor busy. Reserve the elaborate ones for a single hero moment, such as closing a full-screen lightbox, and never put more than one on screen at a time — a grid of spinning, glitching close buttons reads as noise, not polish.