{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shimmer-border",
  "type": "registry:block",
  "title": "Shimmer Border",
  "description": "A CTA whose hairline border carries a comet of light at constant speed around the true perimeter, trailing a tapered tail and spilling glow inside and out. Hover summons a twin comet and a rim spotlight that tracks the pointer; press flares the whole border.",
  "docs": "Shimmer Border was added to public/vividsites/shimmer-border.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/shimmer-border",
  "categories": [
    "ui",
    "buttons-inputs"
  ],
  "files": [
    {
      "path": "components/vividsites/shimmer-border.html",
      "type": "registry:file",
      "target": "public/vividsites/shimmer-border.html",
      "content": "<!--\nvs-shimmer-border: a CTA whose hairline border carries a travelling comet of light.\n  Unlike a rotating conic gradient (which races along the long edges and stalls at the\n  ends), the comet moves at constant speed along the true perimeter, trails a tapered\n  tail, spills a soft glow onto the page and lights the inside of the button as it passes.\n  Hover: the comet speeds up, a twin appears opposite and a spotlight on the rim follows\n  the pointer. Press: the whole rim flares. aria-busy=\"true\": both comets race (loading).\n  disabled / aria-disabled=\"true\": the light dims to a resting hairline.\n\n  Markup:  <a class=\"vs-shimmer-border\" href=\"#\">Label</a>   (or a <button>)\n  Attributes (optional, live):\n    data-period  seconds per lap at rest      (default 3.6)\n    data-tail    tail length, 0–1 of the lap  (default .26)\n\n  Every colour is a CSS variable and can change at any time (no remount needed).\n  Everything is transform/opacity; the loop pauses off-screen and in hidden tabs.\n  Reduced motion: the comet parks on the top edge and only brightens on hover.\n-->\n<a class=\"vs-shimmer-border\" href=\"#\">Get early access\n  <svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" aria-hidden=\"true\"><path d=\"M3 8h9M8.5 4 12.5 8l-4 4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n</a>\n\n<style>\n.vs-shimmer-border {\n  --vs-shimmer-border-bg: #0b0d14;              /* button body */\n  --vs-shimmer-border-ink: #eef1ff;             /* label */\n  --vs-shimmer-border-line: rgba(238,241,255,.12); /* resting hairline */\n  --vs-shimmer-border-head: #ffffff;            /* white-hot head of the comet */\n  --vs-shimmer-border-light: #6d8bff;           /* the comet's colour */\n  --vs-shimmer-border-width: 2px;               /* border thickness */\n  --vs-shimmer-border-radius: 999px;\n  --vs-shimmer-border-font-size: 1rem;          /* scales the padding too */\n  --vs-shimmer-border-glow: .8;                 /* strength of the spill onto the page */\n\n  position: relative; display: inline-flex; vertical-align: middle; border-radius: var(--vs-shimmer-border-radius);\n  text-decoration: none; color: var(--vs-shimmer-border-ink); border: 0; padding: 0; background: none;\n  font-family: inherit; font-size: var(--vs-shimmer-border-font-size); cursor: pointer; outline: none;\n  -webkit-tap-highlight-color: transparent; touch-action: manipulation; user-select: none; -webkit-user-select: none;\n  transition: transform .55s cubic-bezier(.3,1.5,.5,1);\n}\n.vs-shimmer-border, .vs-shimmer-border * { box-sizing: border-box; }\n.vs-shimmer-border:active { transform: scale(.965); transition: transform .12s cubic-bezier(.2,0,0,1); }\n.vs-shimmer-border-body {\n  position: relative; z-index: 1; display: inline-flex; align-items: center; gap: .65em; overflow: hidden;\n  padding: 1.05em 1.75em 1.1em; border-radius: inherit; white-space: nowrap;\n  font-weight: 500; line-height: 1; letter-spacing: -.005em;\n  background: linear-gradient(180deg, color-mix(in srgb, var(--vs-shimmer-border-bg), #fff 6%), var(--vs-shimmer-border-bg) 60%);\n  box-shadow: inset 0 0 0 1px var(--vs-shimmer-border-line), inset 0 1px 0 rgba(255,255,255,.07), 0 10px 30px -12px rgba(0,0,0,.6);\n}\n.vs-shimmer-border-label { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: .65em; transition: opacity .25s, filter .25s; }\n.vs-shimmer-border-body svg { width: .95em; height: .95em; transition: transform .45s cubic-bezier(.16,1,.3,1); }\n.vs-shimmer-border:hover .vs-shimmer-border-body svg, .vs-shimmer-border:focus-visible .vs-shimmer-border-body svg { transform: translateX(3px); }\n.vs-shimmer-border-ring, .vs-shimmer-border-glow { position: absolute; inset: 0; border-radius: inherit; pointer-events: none; }\n.vs-shimmer-border-ring {\n  z-index: 2; padding: var(--vs-shimmer-border-width); overflow: hidden;\n  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude;\n}\n.vs-shimmer-border-flash { position: absolute; inset: 0; background: var(--vs-shimmer-border-light); opacity: 0; will-change: opacity; }\n.vs-shimmer-border-spot {\n  position: absolute; inset: 0; opacity: 0; transition: opacity .35s cubic-bezier(.2,0,0,1);\n  background: radial-gradient(5em circle at var(--mx, 50%) var(--my, 50%), color-mix(in srgb, var(--vs-shimmer-border-light) 70%, #fff), transparent 70%);\n}\n.vs-shimmer-border.is-hot .vs-shimmer-border-spot { opacity: .9; }\n.vs-shimmer-border-glow { z-index: 0; filter: blur(7px); opacity: var(--vs-shimmer-border-glow); }\n.vs-shimmer-border-dot, .vs-shimmer-border-spill { position: absolute; left: 0; top: 0; border-radius: 50%; will-change: transform, opacity; pointer-events: none; }\n.vs-shimmer-border-dot { background: radial-gradient(closest-side, color-mix(in srgb, var(--vs-shimmer-border-light) var(--t), var(--vs-shimmer-border-head)), transparent); }\n.vs-shimmer-border-spill { z-index: 0; background: radial-gradient(closest-side, color-mix(in srgb, var(--vs-shimmer-border-light) 24%, transparent), transparent); mix-blend-mode: screen; }\n.vs-shimmer-border-spin { position: absolute; left: 50%; top: 50%; width: 1.1em; height: 1.1em; margin: -.55em 0 0 -.55em; z-index: 2; opacity: 0; transition: opacity .25s; }\n.vs-shimmer-border-spin circle { fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-dasharray: 14 44; transform-origin: 50% 50%; animation: vs-shimmer-border-spin .8s linear infinite; }\n@keyframes vs-shimmer-border-spin { to { transform: rotate(360deg); } }\n.vs-shimmer-border[aria-busy=\"true\"] { cursor: progress; }\n.vs-shimmer-border[aria-busy=\"true\"] .vs-shimmer-border-label { opacity: 0; filter: blur(4px); }\n.vs-shimmer-border[aria-busy=\"true\"] .vs-shimmer-border-spin { opacity: 1; }\n.vs-shimmer-border:disabled, .vs-shimmer-border[aria-disabled=\"true\"] { cursor: not-allowed; opacity: .45; }\n.vs-shimmer-border:focus-visible .vs-shimmer-border-body { box-shadow: inset 0 0 0 1px var(--vs-shimmer-border-light), 0 0 0 3px color-mix(in srgb, var(--vs-shimmer-border-light) 38%, transparent); }\n@media (forced-colors: active) { .vs-shimmer-border-body { border: 1px solid ButtonText; } .vs-shimmer-border:focus-visible { outline: 2px solid Highlight; outline-offset: 3px; } }\n</style>\n\n<script>\n(() => {\n  if (window.vsShimmerBorder) return;\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const K = 14;                                   // blobs per comet tail\n\n  // a point on a rounded rectangle's outline, by arc length s (starts top-left, clockwise)\n  function outline(W, H, r) {\n    r = Math.min(r, W / 2, H / 2);\n    const a = W - 2 * r, b = H - 2 * r, q = Math.PI * r / 2, P = 2 * a + 2 * b + 4 * q;\n    const arc = (cx, cy, a0, t) => [cx + r * Math.cos(a0 + t / r), cy + r * Math.sin(a0 + t / r)];\n    const at = s => {\n      s = ((s % P) + P) % P;\n      if (s < a) return [r + s, 0]; s -= a;\n      if (s < q) return arc(W - r, r, -Math.PI / 2, s); s -= q;\n      if (s < b) return [W, r + s]; s -= b;\n      if (s < q) return arc(W - r, H - r, 0, s); s -= q;\n      if (s < a) return [W - r - s, H]; s -= a;\n      if (s < q) return arc(r, H - r, Math.PI / 2, s); s -= q;\n      if (s < b) return [0, H - r - s]; s -= b;\n      return arc(r, r, Math.PI, s);\n    };\n    return { P, at };\n  }\n\n  const live = new Set(); let raf = 0, last = 0;\n  const tick = now => {\n    const dt = Math.min(.064, (now - last) / 1000 || .016); last = now;\n    for (const c of live) if (!c.update(dt)) live.delete(c);\n    raf = live.size ? requestAnimationFrame(tick) : 0;\n  };\n  const wake = c => { live.add(c); if (!raf) { last = performance.now(); raf = requestAnimationFrame(tick); } };\n\n  function mount(el) {\n    if (el.__vsSb) return; el.__vsSb = true;\n    const mk = (cls, parent) => { const n = document.createElement('span'); n.className = cls; if (parent) parent.appendChild(n); return n; };\n    const body = mk('vs-shimmer-border-body'), label = mk('vs-shimmer-border-label');\n    while (el.firstChild) label.appendChild(el.firstChild);\n    body.appendChild(label);\n    body.insertAdjacentHTML('beforeend', '<svg class=\"vs-shimmer-border-spin\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"9\"/></svg>');\n    const ring = mk('vs-shimmer-border-ring'), glow = mk('vs-shimmer-border-glow');\n    const flash = mk('vs-shimmer-border-flash', ring), spot = mk('vs-shimmer-border-spot', ring);\n    [ring, glow].forEach(n => n.setAttribute('aria-hidden', 'true'));\n    el.append(glow, body, ring);\n\n    // two comets (the second shows on hover / busy); each: ring blobs, glow blobs, one inner spill\n    const comets = [0, 1].map(() => {\n      const dots = [], glows = [];\n      for (let i = 0; i < K; i++) {\n        const d = mk('vs-shimmer-border-dot', ring); d.style.setProperty('--t', Math.round(Math.min(1, i / 4) * 100) + '%'); dots.push(d);\n        if (i % 2 === 0) { const g = d.cloneNode(); g.style.setProperty('--t', i ? '100%' : '55%'); glow.appendChild(g); glows.push(g); }\n      }\n      const spill = mk('vs-shimmer-border-spill'); body.insertBefore(spill, label);\n      return { dots, glows, spill };\n    });\n\n    let W = 0, H = 0, path = null, s = 0, visible = true, period = 3.6, tailF = .26;\n    const readAttrs = () => { period = Math.max(.4, parseFloat(el.dataset.period) || 3.6); tailF = Math.min(.9, Math.max(.05, parseFloat(el.dataset.tail) || .26)); wake(api); };\n    const measure = () => {\n      W = el.offsetWidth; H = el.offsetHeight;\n      path = outline(W, H, parseFloat(getComputedStyle(body).borderTopLeftRadius) || 0);\n      const sz = Math.max(28, H * 1.15);\n      comets.forEach(c => {\n        c.dots.forEach((d, i) => { const z = sz * (1 - i / K * .45); d.style.width = d.style.height = z + 'px'; d.__z = z; });\n        c.glows.forEach((d, i) => { const z = sz * .62 * (1 - i / K * .5); d.style.width = d.style.height = z + 'px'; d.__z = z; });\n        const z = Math.max(W, H) * .9; c.spill.style.width = c.spill.style.height = z + 'px'; c.spill.__z = z;\n      });\n      wake(api);\n    };\n    new ResizeObserver(measure).observe(el);\n\n    const sp = (x, k, c) => ({ x, v: 0, g: x, k, c });\n    const S = { speed: sp(1, 60, 14), twin: sp(0, 90, 18), fl: sp(0, 240, 26), br: sp(.8, 90, 18) };\n    const place = (c, headS, amp) => {\n      const tail = path.P * tailF, gap = tail / K;\n      c.dots.forEach((d, i) => {\n        const [x, y] = path.at(headS - i * gap);\n        d.style.transform = `translate3d(${(x - d.__z / 2).toFixed(1)}px,${(y - d.__z / 2).toFixed(1)}px,0)`;\n        d.style.opacity = (amp * Math.pow(1 - i / K, 1.5)).toFixed(3);\n      });\n      c.glows.forEach((d, j) => {\n        const i = j * 2, [x, y] = path.at(headS - i * gap);\n        d.style.transform = `translate3d(${(x - d.__z / 2).toFixed(1)}px,${(y - d.__z / 2).toFixed(1)}px,0)`;\n        d.style.opacity = (amp * Math.pow(1 - i / K, 2)).toFixed(3);\n      });\n      const [x, y] = path.at(headS - gap * 2);\n      c.spill.style.transform = `translate3d(${(x - c.spill.__z / 2).toFixed(1)}px,${(y - c.spill.__z / 2).toFixed(1)}px,0)`;\n      c.spill.style.opacity = Math.min(1, amp).toFixed(3);\n    };\n    const api = { update(dt) {\n      if (!path || !W) return false;\n      let settling = false;\n      for (const k in S) { const q = S[k]; if (Math.abs(q.v) < .001 && Math.abs(q.x - q.g) < .001) { q.x = q.g; q.v = 0; continue; }\n        settling = true; const n = Math.max(1, Math.round(dt * 120)), h = dt / n;\n        for (let i = 0; i < n; i++) { q.v += (-q.k * (q.x - q.g) - q.c * q.v) * h; q.x += q.v * h; } }\n      const still = reduce.matches;\n      if (!still) s += dt * S.speed.x * path.P / period;\n      const base = still ? path.P * .18 + path.P * tailF : s;\n      const amp = S.br.x + S.fl.x * .6;\n      place(comets[0], base, amp);\n      place(comets[1], base + path.P / 2, S.twin.x * amp);\n      flash.style.opacity = (S.fl.x * .55).toFixed(3);\n      return (visible && !document.hidden && !still) || settling;\n    }};\n    const run = () => wake(api);\n    new IntersectionObserver(e => { visible = e[0].isIntersecting; if (visible) run(); }).observe(el);\n    document.addEventListener('visibilitychange', () => { if (!document.hidden) run(); });\n\n    const inert = () => el.disabled || el.getAttribute('aria-disabled') === 'true';\n    const busy = () => el.getAttribute('aria-busy') === 'true';\n    let hot = false;\n    const sync = () => {\n      const b = busy(), d = inert();\n      S.speed.g = d ? .35 : b ? 3.4 : hot ? 2.3 : 1;\n      S.twin.g = !d && (hot || b) ? 1 : 0;\n      S.br.g = d ? .18 : hot || b ? 1 : .8;\n      el.classList.toggle('is-hot', hot && !d && !b); run();\n    };\n    new MutationObserver(m => { if (m.some(r => r.attributeName.startsWith('data-'))) readAttrs(); sync(); })\n      .observe(el, { attributes: true, attributeFilter: ['data-period', 'data-tail', 'aria-busy', 'aria-disabled', 'disabled'] });\n    const aim = e => { const r = el.getBoundingClientRect(); ring.style.setProperty('--mx', (e.clientX - r.left) + 'px'); ring.style.setProperty('--my', (e.clientY - r.top) + 'px'); };\n    el.addEventListener('pointerenter', e => { aim(e); if (e.pointerType === 'mouse') { hot = true; sync(); } });\n    el.addEventListener('pointermove', aim, { passive: true });\n    el.addEventListener('pointerleave', () => { hot = false; sync(); });\n    el.addEventListener('focus', () => { if (el.matches(':focus-visible')) { hot = true; sync(); } });\n    el.addEventListener('blur', () => { hot = false; sync(); });\n    const flare = () => { if (inert()) return; S.fl.g = 0; S.fl.x = 1; S.fl.v = 0; run(); };\n    el.addEventListener('pointerdown', e => { flare(); if (e.pointerType !== 'mouse') { hot = true; sync(); } });\n    el.addEventListener('pointerup', e => { if (e.pointerType !== 'mouse') setTimeout(() => { hot = false; sync(); }, 600); });\n    el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') flare(); });\n    readAttrs(); measure(); sync();\n  }\n  const init = () => document.querySelectorAll('.vs-shimmer-border').forEach(mount);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n  window.vsShimmerBorder = { mount };\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Buttons & inputs",
    "tags": [
      "button",
      "border",
      "shimmer",
      "glow",
      "comet",
      "cta",
      "loading"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [],
    "source": null,
    "free": true,
    "accent": "#6d8bff",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/shimmer-border/index.html",
    "thumbnail": "https://vividsites.app/api/component/shimmer-border/thumb.jpg",
    "video": "https://vividsites.app/api/component/shimmer-border/thumb.mp4",
    "page": "https://vividsites.app/c/shimmer-border"
  }
}