← Buttons

Border Draw Button

Hover the button and its border draws itself in: two lines set off from opposite corners and race round the edge until they meet. Move away and they wind back to where they started. It follows the button’s own rounded corners.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-border-draw-button to a button or link. A light or invisible border of its own looks best.

Options

Add any of these attributes to the same element.

data-colorColour of the line. Default: the text colour.
data-widthLine width in pixels. Default 2.
data-durationHow long the drawing takes. Default 650 ms.
data-autoplayIt draws and undraws itself every couple of seconds while in view. Handy for demos.

Keyboard focus draws it too, and on touch screens a tap draws it for a moment.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Border Draw Button: hover the button and its border traces itself in,
    // two lines setting off from opposite corners and racing round until they
    // meet. Move away and the lines wind back to where they started. Keyboard
    // focus draws it too, and a tap draws it on touch screens.
    // Usage: <button data-border-draw-button>Read more</button>
    // Optional: data-color (line colour, default the text colour), data-width
    // (line width in px, default 2), data-duration (ms, default 650),
    // data-autoplay (it draws and undraws itself every couple of seconds while
    // in view; handy for demos).
    var DEFAULTS = { width: 2, duration: 650, every: 2600 };
    var NS = 'http://www.w3.org/2000/svg';

    var css =
      '.bdw{position:relative}' +
      '.bdw-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}' +
      '.bdw-svg path{fill:none;stroke:var(--bdw-color,currentColor);stroke-linecap:round;stroke-linejoin:round}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    // Two halves of a rounded rectangle: the first runs from the top-left
    // corner across the top and down the right; the second from the
    // bottom-right corner along the bottom and up the left.
    function halves(w, h, r, s) {
      var x0 = s / 2, y0 = s / 2, x1 = w - s / 2, y1 = h - s / 2;
      r = Math.max(0, Math.min(r - s / 2, (x1 - x0) / 2, (y1 - y0) / 2));
      var a = ' A' + r + ' ' + r + ' 0 0 1 ';
      return [
        'M' + x0 + ' ' + (y0 + r) + a + (x0 + r) + ' ' + y0 + ' H' + (x1 - r) + a + x1 + ' ' + (y0 + r) +
          ' V' + (y1 - r) + a + (x1 - r) + ' ' + y1,
        'M' + (x1 - r) + ' ' + y1 + ' H' + (x0 + r) + a + x0 + ' ' + (y1 - r) + ' V' + (y0 + r)
      ];
    }

    function setup(el) {
      var d = el.dataset;
      var width = +d.width || DEFAULTS.width;
      var duration = reduced() ? 0 : +d.duration || DEFAULTS.duration;
      el.classList.add('bdw');
      if (d.color) el.style.setProperty('--bdw-color', d.color);

      var svg = document.createElementNS(NS, 'svg');
      svg.setAttribute('class', 'bdw-svg');
      svg.setAttribute('aria-hidden', 'true');
      var paths = [0, 1].map(function () {
        var p = document.createElementNS(NS, 'path');
        p.setAttribute('pathLength', '1');
        p.setAttribute('stroke-width', width);
        p.style.strokeDasharray = '1 1';
        p.style.strokeDashoffset = '1';
        svg.appendChild(p);
        return p;
      });
      el.appendChild(svg);

      function shape() {
        var w = el.offsetWidth, h = el.offsetHeight;
        var r = parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0;
        var ds = halves(w, h, r, width);
        paths[0].setAttribute('d', ds[0]);
        paths[1].setAttribute('d', ds[1]);
      }
      shape();
      if (window.ResizeObserver) new ResizeObserver(shape).observe(el);

      var drawn = false;
      function draw(on) {
        if (on === drawn) return;
        drawn = on;
        anime.animate(paths, {
          strokeDashoffset: on ? 0 : 1,
          duration: on ? duration : duration * 0.8,
          ease: on ? 'inOut(3)' : 'inOut(2)'
        });
      }
      function busy() { return el.matches(':hover') || document.activeElement === el; }

      el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') draw(true); });
      el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch' && document.activeElement !== el) draw(false); });
      el.addEventListener('focus', function () { draw(true); });
      el.addEventListener('blur', function () { if (!el.matches(':hover')) draw(false); });
      var touchTimer = null;
      el.addEventListener('pointerdown', function (e) {
        if (e.pointerType !== 'touch') return;
        clearTimeout(touchTimer);
        draw(true);
        touchTimer = setTimeout(function () { draw(false); }, 1400);
      });

      if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
        var timer = null;
        var pass = function () {
          if (!busy()) {
            draw(true);
            setTimeout(function () { if (!busy()) draw(false); }, DEFAULTS.every * 0.55);
          }
          timer = setTimeout(pass, DEFAULTS.every);
        };
        new IntersectionObserver(function (entries) {
          clearTimeout(timer);
          if (entries[0].isIntersecting) timer = setTimeout(pass, 500);
        }).observe(el);
      }
    }

    function init() {
      document.querySelectorAll('[data-border-draw-button]').forEach(function (el) {
        if (el._bdw) return;
        el._bdw = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<a class="button" href="/blog" data-border-draw-button>Read more</a>

<!-- a thicker pink line: -->
<a class="button" href="/work" data-border-draw-button data-color="#db2777" data-width="3">View the work</a>