Visual Effects

Animated Beam

A gradient comet that travels between two elements.

The Animated Beam draws a path between two elements and sends a gradient comet along it. Point it at any two nodes with template refs and it measures their positions itself, so the beam survives resizes, theme changes and layout shifts.

Give it a curvature and the path bows along the perpendicular of the line between the two refs, which means a vertical beam curves sideways and a diagonal one curves diagonally.

Usage

<template>
  <div
    ref="container"
    class="relative flex h-64 w-full items-center justify-between rounded-xl border border-default bg-default px-10"
  >
    <div
      ref="from"
      class="z-10 flex size-12 items-center justify-center rounded-full border border-default bg-elevated"
    >
      <UIcon
        name="i-lucide-user"
        class="size-5 text-highlighted"
      />
    </div>

    <div
      ref="to"
      class="z-10 flex size-12 items-center justify-center rounded-full border border-default bg-elevated"
    >
      <UIcon
        name="i-lucide-database"
        class="size-5 text-highlighted"
      />
    </div>

    <MAnimatedBeam
      :container-ref="container"
      :from-ref="from"
      :to-ref="to"
      :curvature="-40"
    />
  </div>
</template>

<script setup lang="ts">
const container = ref<HTMLElement | null>(null)
const from = ref<HTMLElement | null>(null)
const to = ref<HTMLElement | null>(null)
</script>

Examples

Reverse sweep

The beam always measures the elements you hand it, so there is nothing to configure in the playground — the three refs are the configuration. Here the same beam runs bottom to top through a wider curve.

<template>
  <div
    ref="container"
    class="relative flex h-72 w-full flex-col items-center justify-between rounded-xl border border-default bg-default py-8"
  >
    <div
      ref="from"
      class="z-10 flex size-12 items-center justify-center rounded-full border border-default bg-elevated"
    >
      <UIcon
        name="i-lucide-cloud"
        class="size-5 text-highlighted"
      />
    </div>

    <div
      ref="to"
      class="z-10 flex size-12 items-center justify-center rounded-full border border-default bg-elevated"
    >
      <UIcon
        name="i-lucide-server"
        class="size-5 text-highlighted"
      />
    </div>

    <MAnimatedBeam
      :container-ref="container"
      :from-ref="from"
      :to-ref="to"
      :curvature="70"
      :duration="3"
      reverse
    />
  </div>
</template>

<script setup lang="ts">
const container = ref<HTMLElement | null>(null)
const from = ref<HTMLElement | null>(null)
const to = ref<HTMLElement | null>(null)
</script>

Props

Prop Type Default Description
containerRefHTMLElement | nullnullThe positioned element the beam is drawn inside. The <svg> overlay is sized to it. Required: the beam renders nothing without it.
fromRefHTMLElement | nullnullThe element the beam leaves from. Its centre is the start of the path. Required.
toRefHTMLElement | nullnullThe element the beam arrives at. Its centre is the end of the path. Required.
curvaturenumber0How far the path bows away from the straight line, in pixels. Negative values bow the other way.
durationnumber4Seconds for one sweep.
delaynumber0Seconds to wait before the first sweep.
sizenumber72Arc length of the travelling comet, in pixels. It is clamped so the comet can never fill the whole path.
reversebooleanfalseSweep from toRef back towards fromRef.
pathWidthnumber2Thickness of both the static path and the comet, in pixels.
pathColorstring'var(--ui-border)'Colour of the static path the comet rides along.
pathOpacitynumber1Opacity of the static path.
gradientStartColorstring'var(--ui-primary)'Colour the comet's tail fades from, at zero opacity.
gradientStopColorstring'var(--ui-secondary)'Colour the comet's head fades up to.
startXOffsetnumber0Nudge the start point along the x axis.
startYOffsetnumber0Nudge the start point along the y axis.
endXOffsetnumber0Nudge the end point along the x axis.
endYOffsetnumber0Nudge the end point along the y axis.
classstring''CSS classes to apply to the <svg> overlay.

UI

This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.animatedBeam in app.config.ts.

Prop Type Default Description
uiobject-Per-slot classes merged over the theme defaults.
Slot Props Description
svg-pointer-events-none absolute inset-0 h-full w-full overflow-visible
Copyright © 2026