Skip to content

Stroke Draw Button

A button whose outline draws itself around the shape on hover, at any size.

Freeminimaleditorialbrutalist
Category
Components
Added
2026-09-20
Deps
none
Updated
2026-09-20
Preview

Props

600
10
Two strokes
No runtime dependencies
"use client";

import { useState } from "react";

export function HoverStrokeDrawButton({
  label = "Install",
  speed = 600,
  radius = 10,
  doubleStroke = true,
}: {
  label?: string;
  speed?: number;
  radius?: number;
  /** Two strokes meeting at the far corners, or one lap of the whole path. */
  doubleStroke?: boolean;
}) {
  const [hovered, setHovered] = useState(false);

  return (
    <button
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onFocus={() => setHovered(true)}
      onBlur={() => setHovered(false)}
      className="relative isolate px-8 py-4 font-mono text-[12px] uppercase tracking-[0.18em] text-white"
    >
      <svg
        aria-hidden
        className="pointer-events-none absolute inset-0 -z-10 h-full w-full overflow-visible"
        preserveAspectRatio="none"
      >
        {/* Resting track, so the button still has a shape before hover. */}
        <rect
          x="1"
          y="1"
          width="calc(100% - 2px)"
          height="calc(100% - 2px)"
          rx={radius}
          fill="none"
          stroke="currentColor"
          strokeOpacity={0.18}
          strokeWidth={1}
        />
        {/*
         * pathLength={1} normalises the geometry so dasharray/dashoffset are
         * plain 0-1 fractions. That is what makes this size-independent — no
         * getTotalLength(), no remeasure on resize, and the same two numbers
         * work whether the button is 100px or 400px wide.
         *
         * dasharray "0.5 0.5" + offset 0.5 -> two strokes meeting at the far
         * corners; "1 1" + offset 1 -> one stroke travelling the whole path.
         */}
        <rect
          x="1"
          y="1"
          width="calc(100% - 2px)"
          height="calc(100% - 2px)"
          rx={radius}
          fill="none"
          stroke="#3d7bff"
          strokeWidth={1.5}
          pathLength={1}
          strokeDasharray={doubleStroke ? "0.5 0.5" : "1 1"}
          strokeDashoffset={hovered ? 0 : doubleStroke ? 0.5 : 1}
          style={{
            transition:
              "stroke-dashoffset " + speed + "ms cubic-bezier(0.16,1,0.3,1)",
          }}
        />
      </svg>
      {label}
    </button>
  );
}
Notes

About this component

Self-drawing borders are usually built by measuring the path with getTotalLength, writing that number into stroke-dasharray, and re-measuring whenever the element resizes — which is why so many of them break the moment the label changes or the viewport narrows. Setting pathLength to 1 removes the problem entirely: SVG then rescales the path's own length to 1, so dasharray and dashoffset become plain fractions that mean the same thing at 100px or 400px wide. No measurement, no resize observer, no layout read at all. From there the variants are just arithmetic — a dasharray of one-half with a matching offset sends two strokes around opposite halves to meet at the far corners, while a whole-length dash sends a single line around the full perimeter. Both animate only stroke-dashoffset, which the compositor handles without touching layout. Worth pairing with a resting track underneath so the control still reads as a button before anyone hovers it, and worth wiring to focus as well as hover so it is not a mouse-only affordance.

Curator’s note

Built for viberdy 2.0. pathLength={1} is the detail that makes this size-independent — most versions of this effect hard-code a dash length and break on resize.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

A fill that wipes in from the edge your pointer crossed and leaves by the one you exit — the label inverts letter by letter as the edge passes through it.

buttonhoverdirection-aware
NewPro

A label that rolls letter by letter to a second copy, rippling outward from wherever your pointer came in — and settling back toward where it left.

buttonhovertext-animation
NewPro

An SVG arc gauge with a needle and a readout that counts to its value.

gaugesvgmeter