09 / 36
All 36 in 36 Tailwind CSS Button Hover Effects
- 01Tailwind Magnetic Ripple Attraction Button Hover Effect
- 02Tailwind Cyberpunk Holographic Glitch Grid Button
- 03Tailwind Frosted Glassmorphism Shimmer Glow Button
- 04Tailwind Organic Blob Morph Button Hover Effect
- 05Tailwind Dual-Line Intersection Border Draw Button
- 06Tailwind Isometric Tactile 3D Depress Button
- 07Tailwind Infinite Sliding Kinetic Marquee Button
- 08Tailwind Chroma Conic Gradient Border Spin Button
- 09Tailwind Minimalist Border & Outline Drawing Hover Effects
- 10Tailwind Modern 3D Skeuomorphic Button Press Effects
- 11Tailwind Neo-Brutalist & Cyberpunk Button Glow Effects
- 12Tailwind Sliding Background Color Wipe Button Effects
- 13Tailwind Liquid Fill & Organic Morphing Button Effects
- 14Tailwind Kinetic Text Flip & Icon Reveal Button Effects
- 15Tailwind Liquid Wave Fill Button Hover Effect
- 16Tailwind Magnetic Border Drift Button
- 17Tailwind Glitch Slice Button Hover Effect
- 18Tailwind Neon Pulse Glow Button Hover Effect
- 19Tailwind Split Reveal Curtain Button Effect
- 20Tailwind Diagonal Shutter Sweep Button Effect
- 21Tailwind Elastic Lift Spring Button Hover Effect
- 22Tailwind Center Underline Draw Link Button Effects
- 23Tailwind 3D Flip Card Button Hover Effect
- 24Tailwind Particle Burst Confetti Button Effect
- 25Tailwind Slide Doors Closing Panel Button Effect
- 26Tailwind Typewriter Retype Terminal Button Effect
- 27Tailwind Hard Shadow Shift Flat Button Effect
- 28Tailwind Material Ripple Wave Button Effect
- 29Tailwind Rainbow Gradient Border Spin Button
- 30Tailwind Morph Shape Silhouette Button Effects
- 31Tailwind Icon Slide In Directional Button Effect
- 32Tailwind Cursor Spotlight Glow Button
- 33Tailwind Stagger Fill Equaliser Button Effect
- 34Tailwind Sunburst Expand Radiant Button Effect
- 35Tailwind 3D Press Plunger Key Button Effect
- 36Tailwind Add-to-Cart Liquid Glass Ripple Button
Tailwind Minimalist Border & Outline Drawing Hover Effects
A refined understated CTA for editorial and luxury layouts: at rest it's just text, and on hover a hairline stroke draws itself continuously around the rounded-rectangle perimeter. Tailwind reaches SVG presentation attributes the same way it reaches anything else it has no scale for — arbitrary properties.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tbh-09-group w-full min-h-svh flex flex-wrap">
<section class="tbh-09a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09a-stage" aria-label="Outline drawing button">
<button type="button" class="group/btn relative cursor-pointer border-0 bg-transparent px-[42px] py-5 text-[15px] font-bold uppercase tracking-[0.06em] text-tbh09a-line focus-visible:outline-none">
<svg aria-hidden="true" preserveAspectRatio="none" class="absolute inset-0 w-full h-full overflow-visible">
<rect x="1" y="1" rx="12" ry="12" class="fill-none [stroke:var(--color-tbh09a-line)] [stroke-width:2] [width:calc(100%-2px)] [height:calc(100%-2px)] [stroke-dasharray:0_1000] transition-[stroke-dasharray] duration-[800ms] ease-out group-hover/btn:[stroke-dasharray:1000_0] group-focus-visible/btn:[stroke-dasharray:1000_0] motion-reduce:transition-none motion-reduce:[stroke-dasharray:1000_0] motion-reduce:opacity-50"></rect>
</svg>
<span class="relative z-[1] group-focus-visible/btn:underline group-focus-visible/btn:underline-offset-4">Read the story</span>
</button>
</section>
<section class="tbh-09b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09b-stage" aria-label="Underline to box button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-8 py-4 text-base font-bold text-tbh09b-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-0 before:w-full before:h-0.5 before:bg-tbh09b-line before:origin-left after:content-[''] after:absolute after:inset-0 after:border-2 after:border-b-0 after:border-tbh09b-line after:[clip-path:inset(0_0_100%_0)] after:transition-[clip-path] after:duration-[350ms] after:delay-150 after:ease-out hover:after:[clip-path:inset(0_0_0_0)] focus-visible:after:[clip-path:inset(0_0_0_0)] focus-visible:underline focus-visible:underline-offset-4 motion-reduce:after:transition-none motion-reduce:after:delay-0">
<span>Discover more</span>
</button>
</section>
</div><div class="tbh-09-group w-full min-h-svh flex flex-wrap">
<section class="tbh-09a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09a-stage" aria-label="Outline drawing button">
<button type="button" class="group/btn relative cursor-pointer border-0 bg-transparent px-[42px] py-5 text-[15px] font-bold uppercase tracking-[0.06em] text-tbh09a-line focus-visible:outline-none">
<svg aria-hidden="true" preserveAspectRatio="none" class="absolute inset-0 w-full h-full overflow-visible">
<rect x="1" y="1" rx="12" ry="12" class="fill-none [stroke:var(--color-tbh09a-line)] [stroke-width:2] [width:calc(100%-2px)] [height:calc(100%-2px)] [stroke-dasharray:0_1000] transition-[stroke-dasharray] duration-[800ms] ease-out group-hover/btn:[stroke-dasharray:1000_0] group-focus-visible/btn:[stroke-dasharray:1000_0] motion-reduce:transition-none motion-reduce:[stroke-dasharray:1000_0] motion-reduce:opacity-50"></rect>
</svg>
<span class="relative z-[1] group-focus-visible/btn:underline group-focus-visible/btn:underline-offset-4">Read the story</span>
</button>
</section>
<section class="tbh-09b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09b-stage" aria-label="Underline to box button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-8 py-4 text-base font-bold text-tbh09b-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-0 before:w-full before:h-0.5 before:bg-tbh09b-line before:origin-left after:content-[''] after:absolute after:inset-0 after:border-2 after:border-b-0 after:border-tbh09b-line after:[clip-path:inset(0_0_100%_0)] after:transition-[clip-path] after:duration-[350ms] after:delay-150 after:ease-out hover:after:[clip-path:inset(0_0_0_0)] focus-visible:after:[clip-path:inset(0_0_0_0)] focus-visible:underline focus-visible:underline-offset-4 motion-reduce:after:transition-none motion-reduce:after:delay-0">
<span>Discover more</span>
</button>
</section>
</div>@import "tailwindcss";
@theme {
--color-tbh09a-line: #4a5068;
--color-tbh09a-line: oklch(0.45 0.03 265);
--color-tbh09a-stage: #f6f5f1;
--color-tbh09a-stage: oklch(0.97 0.006 90);
--color-tbh09b-line: #b04a2f;
--color-tbh09b-line: oklch(0.5 0.14 25);
--color-tbh09b-stage: #faf6f4;
--color-tbh09b-stage: oklch(0.98 0.008 35);
--font-tbh09-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: SVG presentation attributes are reachable as arbitrary properties,
which is the useful discovery in this port. */
.tbh-09-group {
width: 100%;
min-height: 100svh;
}
.tbh-09a,
.tbh-09b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh09a-line: #4a5068;
--color-tbh09a-line: oklch(0.45 0.03 265);
--color-tbh09a-stage: #f6f5f1;
--color-tbh09a-stage: oklch(0.97 0.006 90);
--color-tbh09b-line: #b04a2f;
--color-tbh09b-line: oklch(0.5 0.14 25);
--color-tbh09b-stage: #faf6f4;
--color-tbh09b-stage: oklch(0.98 0.008 35);
--font-tbh09-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: SVG presentation attributes are reachable as arbitrary properties,
which is the useful discovery in this port. */
.tbh-09-group {
width: 100%;
min-height: 100svh;
}
.tbh-09a,
.tbh-09b {
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tbh09a-line: #4a5068;
--color-tbh09a-line: oklch(0.45 0.03 265);
--color-tbh09a-stage: #f6f5f1;
--color-tbh09a-stage: oklch(0.97 0.006 90);
--color-tbh09b-line: #b04a2f;
--color-tbh09b-line: oklch(0.5 0.14 25);
--color-tbh09b-stage: #faf6f4;
--color-tbh09b-stage: oklch(0.98 0.008 35);
--font-tbh09-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: SVG presentation attributes are reachable as arbitrary properties,
which is the useful discovery in this port. */
.tbh-09-group {
width: 100%;
min-height: 100svh;
}
.tbh-09a,
.tbh-09b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh09a-line: #4a5068;
--color-tbh09a-line: oklch(0.45 0.03 265);
--color-tbh09a-stage: #f6f5f1;
--color-tbh09a-stage: oklch(0.97 0.006 90);
--color-tbh09b-line: #b04a2f;
--color-tbh09b-line: oklch(0.5 0.14 25);
--color-tbh09b-stage: #faf6f4;
--color-tbh09b-stage: oklch(0.98 0.008 35);
--font-tbh09-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: SVG presentation attributes are reachable as arbitrary properties,
which is the useful discovery in this port. */
.tbh-09-group {
width: 100%;
min-height: 100svh;
}
.tbh-09a,
.tbh-09b {
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- SVG presentation attributes turn out to port cleanly:
[stroke:var(--color-tbh09a-line)],[stroke-width:2]and[stroke-dasharray:0_1000]all compile, andfill-noneeven has a real utility. The<rect>now carries its resting stroke, its hover target and its reduced-motion state in one class list. transition-[stroke-dasharray]is the part worth remembering — Tailwind's arbitrary transition-property accepts any animatable property name, including SVG ones, so nothing about this effect needed a stylesheet.- UI change from the CSS original: design A's hover draw now also fires on
group-focus-visible/btn:, and its reduced-motion state shows the frame at 50% opacity rather than nothing — keyboard and reduced-motion users previously got the label alone.
Here's a working CSS demo 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: Tailwind Minimalist Border & Outline Drawing Hover Effects
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-minimalist-border-outline-drawing-hover-effects/
A refined understated CTA for editorial and luxury layouts: at rest it's just text, and on hover a hairline stroke draws itself continuously around the rounded-rectangle perimeter. Tailwind reaches SVG presentation attributes the same way it reaches anything else it has no scale for — arbitrary properties.
## HTML
```html
<div class="tbh-09-group w-full min-h-svh flex flex-wrap">
<section class="tbh-09a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09a-stage" aria-label="Outline drawing button">
<button type="button" class="group/btn relative cursor-pointer border-0 bg-transparent px-[42px] py-5 text-[15px] font-bold uppercase tracking-[0.06em] text-tbh09a-line focus-visible:outline-none">
<svg aria-hidden="true" preserveAspectRatio="none" class="absolute inset-0 w-full h-full overflow-visible">
<rect x="1" y="1" rx="12" ry="12" class="fill-none [stroke:var(--color-tbh09a-line)] [stroke-width:2] [width:calc(100%-2px)] [height:calc(100%-2px)] [stroke-dasharray:0_1000] transition-[stroke-dasharray] duration-[800ms] ease-out group-hover/btn:[stroke-dasharray:1000_0] group-focus-visible/btn:[stroke-dasharray:1000_0] motion-reduce:transition-none motion-reduce:[stroke-dasharray:1000_0] motion-reduce:opacity-50"></rect>
</svg>
<span class="relative z-[1] group-focus-visible/btn:underline group-focus-visible/btn:underline-offset-4">Read the story</span>
</button>
</section>
<section class="tbh-09b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09b-stage" aria-label="Underline to box button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-8 py-4 text-base font-bold text-tbh09b-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-0 before:w-full before:h-0.5 before:bg-tbh09b-line before:origin-left after:content-[''] after:absolute after:inset-0 after:border-2 after:border-b-0 after:border-tbh09b-line after:[clip-path:inset(0_0_100%_0)] after:transition-[clip-path] after:duration-[350ms] after:delay-150 after:ease-out hover:after:[clip-path:inset(0_0_0_0)] focus-visible:after:[clip-path:inset(0_0_0_0)] focus-visible:underline focus-visible:underline-offset-4 motion-reduce:after:transition-none motion-reduce:after:delay-0">
<span>Discover more</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh09a-line: #4a5068;
--color-tbh09a-line: oklch(0.45 0.03 265);
--color-tbh09a-stage: #f6f5f1;
--color-tbh09a-stage: oklch(0.97 0.006 90);
--color-tbh09b-line: #b04a2f;
--color-tbh09b-line: oklch(0.5 0.14 25);
--color-tbh09b-stage: #faf6f4;
--color-tbh09b-stage: oklch(0.98 0.008 35);
--font-tbh09-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: SVG presentation attributes are reachable as arbitrary properties,
which is the useful discovery in this port. */
.tbh-09-group {
width: 100%;
min-height: 100svh;
}
.tbh-09a,
.tbh-09b {
min-height: 100svh;
}
```Here's a working CSS demo 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: Tailwind Minimalist Border & Outline Drawing Hover Effects
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-minimalist-border-outline-drawing-hover-effects/
A refined understated CTA for editorial and luxury layouts: at rest it's just text, and on hover a hairline stroke draws itself continuously around the rounded-rectangle perimeter. Tailwind reaches SVG presentation attributes the same way it reaches anything else it has no scale for — arbitrary properties.
## HTML
```html
<div class="tbh-09-group w-full min-h-svh flex flex-wrap">
<section class="tbh-09a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09a-stage" aria-label="Outline drawing button">
<button type="button" class="group/btn relative cursor-pointer border-0 bg-transparent px-[42px] py-5 text-[15px] font-bold uppercase tracking-[0.06em] text-tbh09a-line focus-visible:outline-none">
<svg aria-hidden="true" preserveAspectRatio="none" class="absolute inset-0 w-full h-full overflow-visible">
<rect x="1" y="1" rx="12" ry="12" class="fill-none [stroke:var(--color-tbh09a-line)] [stroke-width:2] [width:calc(100%-2px)] [height:calc(100%-2px)] [stroke-dasharray:0_1000] transition-[stroke-dasharray] duration-[800ms] ease-out group-hover/btn:[stroke-dasharray:1000_0] group-focus-visible/btn:[stroke-dasharray:1000_0] motion-reduce:transition-none motion-reduce:[stroke-dasharray:1000_0] motion-reduce:opacity-50"></rect>
</svg>
<span class="relative z-[1] group-focus-visible/btn:underline group-focus-visible/btn:underline-offset-4">Read the story</span>
</button>
</section>
<section class="tbh-09b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh09-sans bg-tbh09b-stage" aria-label="Underline to box button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-8 py-4 text-base font-bold text-tbh09b-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-0 before:w-full before:h-0.5 before:bg-tbh09b-line before:origin-left after:content-[''] after:absolute after:inset-0 after:border-2 after:border-b-0 after:border-tbh09b-line after:[clip-path:inset(0_0_100%_0)] after:transition-[clip-path] after:duration-[350ms] after:delay-150 after:ease-out hover:after:[clip-path:inset(0_0_0_0)] focus-visible:after:[clip-path:inset(0_0_0_0)] focus-visible:underline focus-visible:underline-offset-4 motion-reduce:after:transition-none motion-reduce:after:delay-0">
<span>Discover more</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh09a-line: #4a5068;
--color-tbh09a-line: oklch(0.45 0.03 265);
--color-tbh09a-stage: #f6f5f1;
--color-tbh09a-stage: oklch(0.97 0.006 90);
--color-tbh09b-line: #b04a2f;
--color-tbh09b-line: oklch(0.5 0.14 25);
--color-tbh09b-stage: #faf6f4;
--color-tbh09b-stage: oklch(0.98 0.008 35);
--font-tbh09-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: SVG presentation attributes are reachable as arbitrary properties,
which is the useful discovery in this port. */
.tbh-09-group {
width: 100%;
min-height: 100svh;
}
.tbh-09a,
.tbh-09b {
min-height: 100svh;
}
```How this works
The stroke is hidden by arithmetic. stroke-dasharray: 0 1000 means a zero-length dash followed by a 1000px gap — an invisible stroke. Hover swaps the two numbers, and because dasharray interpolates, the dash grows around the perimeter as the gap shrinks. One transition, one continuous line.
Why a rect and not a border. A CSS border cannot be drawn progressively; an SVG stroke can. The <svg> sits absolute inset-0 with preserveAspectRatio="none" so the rect tracks the button's real size whatever the label length.
Design B stages two shapes. The bottom rule exists at rest (so the button reads as a link), and hover reveals the remaining three sides top-down via clip-path, 150ms later. The delay is what makes it read as one gesture: rule first, then frame.
Make it yours
- Draw speed:
duration-[800ms]. Slower reads as luxury, faster as utility. - Stroke weight and colour:
[stroke-width:2]and[stroke:var(--color-tbh09a-line)]. - Draw origin: reverse the dasharray pair to start the sweep from the opposite corner.
- Design B's stagger:
after:delay-150— drop it and the underline and frame appear as one flat box.
Gotchas — read before shipping
- The dasharray total must exceed the true perimeter or the loop visibly fails to close. 1000 is generous for a button; measure if you scale the pattern up.
- Add
[vector-effect:non-scaling-stroke]if the SVG is ever scaled, or the hairline thickens with it. - Both designs replace the focus outline with a visible non-motion indicator (an underline on the label). That is the only acceptable way to drop an outline — never remove it for the drawn frame alone, which does not exist until hover.
- Design A's rect is inset by 1px (
x="1" y="1"withcalc(100%-2px)sizing) so the 2px stroke is not clipped at the SVG edge.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
SVG stroke-dash transitions and clip-path: inset() are universal; with CSS unavailable both buttons remain readable text labels.