20 Tailwind SVG Stroke Buttons

17 Pure Tailwind3 Tailwind + JSPublished

20 hand-coded Tailwind SVG stroke buttons whose outlines draw themselves from stroke-dasharray and stroke-dashoffset utilities instead of a static border. These cover draw-on-hover and pathLength normalisation, then gradient strokes, dashed marching-ant crops, focus-visible keyboard outlines, submit loading and disabled states, staggered groups, and scroll-timeline reveals. Scoped under .svgb-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.

Related20 CSS SVG Stroke Buttons51 CSS Buttons20 CSS Loading Buttons22 CSS Gradient Buttons51 Tailwind CSS Buttons

Premium icon pickPartner: Nucleo

Affiliate link

Need icons for the components you just copied?

You just browsed 20 Tailwind svg stroke button designs. Nucleo is a library of 44,000+ SVG icons in five families (Core, UI, Sharp, Pixel and Micro Bold), with web, Mac and Windows apps to manage them. The All Icons Bundle is $149 with lifetime updates.

Get the Nucleo bundle

Nucleo. We may earn a commission, at no extra cost to you.

01. Tailwind SVG Stroke Draw Button Border on Hover (opens in a new tab)

Published

Tailwind v4
The same two-pass pen sketch, written entirely in the markup. Tokens become @theme entries so the ink colour is a real stroke-svgb01-mark utility; the dash constant and the offset ride the paths as arbitrary properties; and group on the button replaces the descendant hover selector, so hovering and tabbing both drive group-hover:[stroke-dashoffset:0]. The only rule left in a stylesheet is the :has() theme flip, which utilities cannot express.

02. Tailwind Stroke Dashoffset Button Outline Animation (opens in a new tab)

Published

Tailwind v4
Three pads, three jobs for one number — ported so the difference between them is readable in the class lists. Both loops are declared once as --animate-* theme entries, so the markup only says animate-svgb02-orbit; hovering flips [animation-play-state:paused] to running rather than restarting the animation; and the twenty waveform bars share a single class list because h-(--h) reads each bar's own inline variable.

03. Tailwind Corner Start Perimeter Draw Button (opens in a new tab)

Published

Tailwind v4
The measured draw, ported with its three layers still reading one clock. duration-[760ms] ease-svgb03 appears on the tape, the ink and the head alike, with the curve declared once in @theme — and reversing a cell needs no modifier class at all, just [stroke-dashoffset:-592] on the path. offset-path stays in CSS, because it takes literal path data and needs an @supports guard.

04. Tailwind Two Path Split Border Draw Button (opens in a new tab)

Published

Tailwind v4
Two halves, one constant — and in Tailwind the constant is literally shared, because both paths carry the same class list character for character. The seam flash is built entirely from pseudo-element variants, from after:content-[''] through hover:after:delay-[620ms], where the delay equals the draw duration so the tear lands exactly as the two heads meet.

05. Tailwind Rounded Rect Stroke Button with rx Radius (opens in a new tab)

Published

Tailwind v4
A radius inspector where picking a value cannot desynchronise the maths, because the radius and its measured perimeter are published by the same checked radio and read in the same breath: [rx:var(--svgb-05-rx,8px)] next to [stroke-dasharray:var(--svgb-05-peri,915)]. The four ghost contours each read their own --svgb-05-g-* opacity variable, so one invalidation point drives all of them.

06. Tailwind Dashed Marching Ants Button Border (opens in a new tab)

Published

Tailwind v4
Marching ants where they belong — a crop selection over a photograph — with the march declared once as --animate-svgb06-march so the marquee and the Apply button cannot drift apart. The only difference between a selection that marches forever and a button that marches on hover is two utilities: [animation-play-state:paused] group-hover:[animation-play-state:running].
Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

07. Tailwind Gradient Stroke Button with SVG linearGradient (opens in a new tab)

Published

Tailwind v4
A gradient border with no background-clip hack anywhere: the stroke is a paint-server reference, written as the arbitrary property [stroke:url(#svgb-07-grad)] because an id lookup is not a colour value. The pill and the badge both draw with [stroke-dasharray:1] [stroke-dashoffset:1] — two very different perimeters, one constant, because both paths carry pathLength="1".

08. Tailwind Stroke Draw Button That Reverses on Mouse Leave (opens in a new tab)

Published

Tailwind v4
Three keycaps, three transition declarations, side by side in the markup where you can actually compare them. The symmetric key reverses from wherever the pointer left it; the broken key uses group-hover:animate-svgb08-draw and snaps, because an animation has no memory of an interrupted position; the third declares a full [transition:...] twice to get 300ms in and 760ms out.

09. Tailwind Focus Visible Stroke Outline for Keyboard Users (opens in a new tab)

Published

Tailwind v4
The accessibility argument of the collection, expressed as a naming convention: every hover utility here has a group-focus-visible: twin, so typing one and not the other leaves a visible hole in the class list. Focus also thickens the stroke and shortens the draw — a keyboard user is waiting on it, not exploring — and the forced-colors block stays in CSS because system keywords are not theme tokens.

10. Tailwind Non Scaling Stroke Button That Resizes with Text (opens in a new tab)

Published

Tailwind v4
Six languages, right-to-left included, each button sized by its own text — and a border that does not draw but wipes, using [clip-path:inset(0_100%_0_0)] instead of a dash constant, so nothing has to know the perimeter at any label width. The Arabic row is the lesson: clip-path takes physical edges only, so it needs rtl:[clip-path:inset(0_0_0_100%)].

11. Tailwind Stroke Draw Button with pathLength Normalization (opens in a new tab)

Published

Tailwind v4
Five silhouettes — hexagon, diamond, shield, octagon, ring — animated by one class string, character for character, apart from colour. Because every path carries pathLength="1", [stroke-dasharray:1] [stroke-dashoffset:1] is a complete outline on all of them, which means the string is genuinely reusable: extract it into an @utility and it animates any shape you point it at.

12. Tailwind Dual Layer Stroke Button with Offset Trailing Path (opens in a new tab)

Published

Tailwind v4
A crisp leader and a blurred follower 90ms behind it, which the eye reads as speed rather than as two lines. The offset is two utilities — duration-[620ms] on the leader, duration-[800ms] delay-[90ms] on the trail — and the comet pair animates the offset negative, so a 4.5% fraction of pathLength="1" travels the path instead of a line being drawn.

13. Tailwind Sequential Stroke and Label Fade Button (opens in a new tab)

Published

Tailwind v4
Four beats in, four beats out — and both ladders are visible on the same elements, because every step carries a delay-[...] for leaving and a group-hover:delay-[...] for arriving. All eight delays are multiples of one variable via delay-[calc(var(--svgb-13-step)*2.6)], so changing the beat retimes the whole cascade, and the second card becomes a two-beat version by setting that variable inline.

14. Tailwind Icon Plus Border Stroke Draw Together (opens in a new tab)

Published

Tailwind v4
A ring hundreds of units round and a glyph a few dozen, drawing in perfect step because both carry pathLength="1" and share one duration-[640ms]. Multi-stroke glyphs are split into separate paths — a dash pattern restarts at every M — and the second gets delay-[60ms], which is the difference between drawn and switched on.
Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

15. Tailwind Stroke Draw Submit Button Loading State (opens in a new tab)

Published

Tailwind v4
Three real states carried by one outline, with the script setting a single attribute and every visual consequence following from data-* variants: data-[state=busy]:bg-svgb15-stroke/7 on the button, group-data-[state=done]:opacity-100 on its children. No class juggling in JavaScript — and group-data-[state=idle]:group-hover:[stroke-dashoffset:0] stacks two variants to say draw on hover, but only while idle.

16. Tailwind Stroke Draw Button Disabled and Error States (opens in a new tab)

Published

Tailwind v4
Three states that must be distinguishable both visually and semantically: a genuinely disabled stepper at the floor, an aria-disabled sign-off a keyboard user can still reach and learn from, and an error frame that appears instantly at full weight. The root carries group/state and each button its own named group, so one class list can react to both: group-data-[state=error]/state: for the screen, group-hover/btn: for the control.

17. Tailwind Scroll Triggered Button Stroke with animation-timeline (opens in a new tab)

Published

Tailwind v4
Scroll-driven strokes with no observer and no listener — the timeline and range are arbitrary properties, [animation-timeline:view()] and [animation-range:entry_100%_contain_55%], which turns out to be the readable form: the two CTAs differ by exactly one class, so you can compare their ranges without opening a stylesheet. The @supports not block stays in CSS and is what makes this safe to ship.

18. Tailwind Stroke Draw Button Group with Staggered Delay (opens in a new tab)

Published

Tailwind v4
A wave across six platform tabs, driven entirely from the markup: group-hover/rail:delay-[calc(var(--i,0)*var(--svgb-18-step))] reads each item's own inline --i, so adding a seventh platform needs no CSS at all. Two named groups do two jobs — group/rail owns the wave, group/tab owns the individual lift — and the pointer-origin version stays in CSS, because there is no variant for two siblings after the hovered one.

19. Tailwind Reduced Motion Fallback for Stroke Buttons (opens in a new tab)

Published

Tailwind v4
The best argument for utilities in the whole collection: Tailwind's motion-reduce: variant is @media (prefers-reduced-motion: reduce), so the fallback lives in the markup next to the thing it replaces — you cannot ship the animation and forget the fallback in another file. And the fallback restates the end state rather than deleting the transition, because a bare transition-none leaves a button that never responds at all.

20. Tailwind Variable Width Stroke Button with Text Length (opens in a new tab)

Published

Tailwind v4
Six labels from three characters to thirty-five, each deriving its stroke weight, dash rhythm and draw duration from one inline number. Hover transitions the dash pattern rather than the offset — hover:[--svgb-20-duty:1] closes every gap until the measure becomes a solid border, a state change no dash-offset draw can express. Both @property blocks stay in CSS, and they are the load-bearing part.
FAQ

Frequently asked questions

How do you animate an SVG stroke to draw a button border with Tailwind utilities?
The border is a real <path> or <rect> inside an inline <svg>, and the draw is two arbitrary-value utilities on that stroke element. Set the dash array to the full perimeter and the offset to the same number so the line starts fully hidden: [stroke-dasharray:400] and [stroke-dashoffset:400]. On hover, drive the offset back to zero with a group variant, e.g. group-hover:[stroke-dashoffset:0], plus a transition-[stroke-dashoffset] and a duration. Because the dash gap equals the visible length, the stroke appears to draw from one point around the shape. Demo 01 ports the two-pass sketch and Demo 02 shows the same one number doing three different jobs, so you can read the dash-array, dash-offset, and gap relationship directly in the class list instead of guessing it from compiled CSS.
Why does my Tailwind stroke button flash a fully-drawn border for a split second on load?
That flash means the idle state is wrong: the stroke is painting its full length before your hover utility ever applies. The fix is to make the resting state the hidden one. Put [stroke-dashoffset:400] (equal to the dash array) on the base element, not inside a variant, so the border is invisible until interaction moves the offset to zero. A second cause is a missing [stroke-dasharray] value — with no dash array the whole path is one solid segment and the offset has nothing to slide against, so the browser renders the complete outline immediately. Verify the two numbers match the true perimeter; if the dash array is shorter than the path, the tail wraps and you see a stub of border at rest. Demos 01 and 03 keep both numbers visible in the markup precisely so this desync is easy to spot before it ships.
How do I keep the stroke width from getting thicker when the SVG button scales with its text?
Use the paint keyword vector-effect="non-scaling-stroke" on the stroke element. Normally an <svg> scales its geometry and its stroke together, so a button sized by a long label ends up with a heavier border than a short one. The non-scaling keyword tells the renderer to draw the stroke at its declared width in screen pixels regardless of the element's transform or viewBox scaling, so a one-pixel outline stays one pixel whether the button holds three characters or thirty. Demo 10 sizes six buttons — including a right-to-left label — entirely by their own text and the border weight never drifts. Note this is an SVG attribute, not a Tailwind utility; you write it on the markup. Pair it with pathLength normalization if you also animate the draw, so the timing stays constant across the different perimeters.
What does pathLength do and why do stroke buttons of different shapes draw at different speeds without it?
By default a dash array is measured in real user units, so a wide rectangle has a longer perimeter than a small ring and the same transition-duration makes the big one appear to draw faster per side. Setting pathLength="100" on the stroke element remaps its measured length to a flat 100 units, so [stroke-dasharray:100] and [stroke-dashoffset:100] mean the same thing on every shape. Now one class string drives a hexagon, a diamond, a shield and a ring at an identical rate, as Demo 11 shows across five silhouettes. Demo 14 uses it to sync a large ring and a small glyph so both finish drawing on the same beat. Without it you would hand-tune a different dash number per shape and re-tune every time the geometry changes — pathLength makes the timing character-count and size independent.
How do I make a Tailwind stroke button respond to keyboard focus, not just mouse hover?
Add the draw to the focus-visible: variant alongside group-hover:, so keyboard users get the same outline animation a mouse triggers: group-hover:[stroke-dashoffset:0] focus-visible:[stroke-dashoffset:0]. The focus-visible variant only fires for keyboard focus, not pointer clicks, so mouse users do not see a lingering ring after they click. Do not remove the native focus ring unless your drawn stroke is genuinely visible against every background — an animated border that only shows on a light panel leaves keyboard users with no indicator on a dark one. Demo 09 treats this as a naming convention: every hover utility has a matching focus-visible twin, which is the whole accessibility argument of the collection. If you strip the browser outline, the stroke draw is your focus indicator, so it must meet contrast on its own.
How do you build a loading or submit state on a stroke button without JavaScript rewriting the classes?
Drive one attribute from script and let variants do the rest. Set something like data-state="loading" on the button, then style with an arbitrary variant such as data-[state=loading]:[stroke-dashoffset:0] or an animated dash for a spinner effect. The script sets a single attribute; every visual consequence — the outline, the label fade, the disabled look — is expressed declaratively in the markup, which is exactly how Demo 15 carries three real states through one outline. For a genuine disabled state, use the real disabled attribute (Demo 16) so the control is non-focusable and announced correctly, and pair it with a disabled: variant for the muted styling. Avoid faking disabled with only a class — that leaves the button clickable and reachable by keyboard, which is a functional bug, not just a visual one.
How can I draw a stroke button on scroll using animation-timeline instead of an IntersectionObserver?
Demo 17 uses the scroll-driven animations spec: declare a keyframe that moves stroke-dashoffset from the full length to zero, then attach it with an arbitrary [animation-timeline:view()] and an [animation-range:...] so the draw is tied to the element's position in the viewport rather than to a hover or a timer. There is no observer and no scroll listener — the browser advances the animation as you scroll and rewinds it as you scroll back. The tradeoff is support: animation-timeline is not implemented everywhere, so treat the scroll draw as an enhancement over a visible resting state, never as the only way the border appears. If the property is unsupported the keyframe simply does not run, so make sure the un-animated button already looks complete on its own.
When should I not use an animated SVG stroke button, and how do I respect reduced motion?
Skip the draw animation for anything a user hits repeatedly under time pressure — a form submit they retry, a primary action in a dense toolbar — where a 700ms border sketch reads as lag rather than polish; there a plain static outline serves better. Always gate the motion behind Tailwind's motion-reduce: variant, which maps to the prefers-reduced-motion media query. Demo 19 makes this the strongest case for utilities: write motion-reduce:transition-none or motion-reduce:[stroke-dashoffset:0] right beside the animated utility, so the reduced-motion path is visible in the same class list instead of buried in a separate block. The rule is that the border must reach its final visible state instantly for those users — never leave it stuck at a hidden stroke-dashoffset, or reduced-motion visitors get a button with no outline at all.

Related collections

15 CSS 3D Tilt Hover Cards preview

15 CSS 3D Tilt Hover Cards

15 mouse-aware 3D tilt hover cards — e-commerce product spotlight, glassmorphism parallax team card, interactive pricing tier, holographic NFT collectible, pop-out mascot, dark tech grid with border glow, media player album art, blog article preview, cyberpunk neon glow, dashboard KPI widget, minimalist real estate, flip-to-back tilt, mobile app showcase, course learning card, and a Pure CSS touch-friendly tilt. Vanilla JS writes --rx/--ry/--mx/--my; all rendering stays in CSS on the GPU.

24 CSS Animated Cards preview

24 CSS Animated Cards

24 hand-coded CSS animated card patterns organised by what triggers the motion, not by card style. Scroll and entrance triggers: staggered grid reveals, @starting-style mount-in, scroll-driven scale and fade on animation-timeline: view(), View Transitions API card-to-detail morphs, blur-to-sharp lazy image loading, and FLIP re-layout when a filter changes. State-change triggers: add-to-cart success morph, like and save particle burst, expand and collapse to intrinsic height with calc-size(), swipe-to-dismiss on Pointer Events, and skeleton-to-loaded crossfade. Data-driven triggers: count-up KPI metrics, SVG sparkline draw-on-enter, and progress ring fill on load. Ambient idle loops: breathing glow, floating drift, animated gradient mesh backgrounds, conic rotating borders, auto-cycling testimonial decks, and live status pulses. Plus four hover patterns where the effect is the topic itself: cursor spotlight, holographic foil glare, corner ribbon slide, and horizontal accordion expand. Every card is scoped under a .ac-NN prefix for no-collision pasting, guards prefers-reduced-motion, animates compositor-only properties, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.

22 CSS Avatars preview

22 CSS Avatars

22 hand-coded CSS avatars for chat apps, team dashboards, comment threads, account menus, and social profiles. Covers circular and squircle shapes, gradient and conic story rings, hexagon clip-path crops, online status dots, notification count badges, verified checkmarks, stacked facepiles with plus-N overflow, initials fallbacks for users without photos, broken-image recovery, avatar pickers, and a size scale system where the ring, badge, and status dot all scale from a single custom property.

Search CodeFronts

Loading…