Menu

Change language

GitHub repo

Border Beam

border-beamui

Rotating conic light beam that traces the border ring of its parent. Place inside a `relative` parent (e.g. a Card); the overlay is absolutely positioned, pointer-events-none, and masked down to a ring of `size` px thickness. Tune `duration`, `delay`, and `color`, or set `paused` to freeze it. Keyframes and the `--uipkge-border-angle` @property ship in the canonical tailwind tokens — re-pull init/tailwind if upgrading.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/border-beam.json
Manifest:https://uipkge.dev/r/lit/border-beam.jsonInstalls to:src/uip/components/border-beam/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
sizenumber2optional
durationnumber6optional
delaynumber0optional
colorstringvar(--primary)optional
pausedbooleanfalseoptional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-border-beam': UipBorderBeam
  }
}

npm dependencies

Includes

Files installed (1)

  • src/uip/components/border-beam/border-beam.ts3.4 kB
    import { LitElement, css, html, isServer } from 'lit'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    
    if (!isServer && typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
      try {
        CSS.registerProperty({
          name: '--uipkge-border-angle',
          syntax: '<angle>',
          inherits: false,
          initialValue: '0deg',
        })
      } catch {
        // already registered
      }
    }
    
    const beamStyles = css`
      @keyframes uipkge-border-beam {
        to {
          --uipkge-border-angle: 360deg;
        }
      }
    `
    
    /**
     * <uip-border-beam> — a conic highlight that traces an element's border ring.
     */
    export class UipBorderBeam extends LitElement {
      static styles = [
        tailwind,
        beamStyles,
        css`
          :host {
            display: block;
            position: absolute;
            inset: 0;
            pointer-events: none;
            border-radius: inherit;
          }
        `,
      ]
    
      static properties = {
        size: { type: Number, reflect: true },
        duration: { type: Number, reflect: true },
        delay: { type: Number, reflect: true },
        color: { reflect: true },
        paused: { type: Boolean, reflect: true },
        reducedMotion: { state: true },
      }
    
      size = 2
      duration = 6
      delay = 0
      color = 'var(--primary)'
      paused = false
      private reducedMotion = false
      private mediaQueryHandler?: (e: MediaQueryListEvent) => void
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'border-beam')
        this.setAttribute('aria-hidden', 'true')
    
        if (!isServer) {
          const query = window.matchMedia('(prefers-reduced-motion: reduce)')
          this.reducedMotion = query.matches
          this.mediaQueryHandler = (e: MediaQueryListEvent) => {
            this.reducedMotion = e.matches
          }
          query.addEventListener('change', this.mediaQueryHandler)
        }
      }
    
      disconnectedCallback() {
        super.disconnectedCallback()
        if (!isServer && this.mediaQueryHandler) {
          window.matchMedia('(prefers-reduced-motion: reduce)').removeEventListener('change', this.mediaQueryHandler)
        }
      }
    
      render() {
        const background = this.reducedMotion
          ? `conic-gradient(from 45deg, transparent 0deg, transparent 290deg, ${this.color} 330deg, transparent 360deg)`
          : `conic-gradient(from var(--uipkge-border-angle), transparent 0deg, transparent 290deg, ${this.color} 330deg, transparent 360deg)`
        const animation = this.reducedMotion ? 'none' : `uipkge-border-beam ${this.duration}s linear infinite ${this.delay}s`
        const playState = !this.reducedMotion && this.paused ? 'paused' : 'running'
        const style = `padding: ${this.size}px; background: ${background}; animation: ${animation}; animation-play-state: ${playState}; -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude;`
    
        return html`
          <span
            part="base"
            data-slot="border-beam"
            aria-hidden="true"
            class="pointer-events-none absolute inset-0 rounded-[inherit]"
            style=${style}
          ></span>
        `
      }
    }
    
    customElements.get('uip-border-beam') || customElements.define('uip-border-beam', UipBorderBeam)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-border-beam': UipBorderBeam
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/border-beam.json