Animated Beam
An animated beam of light which travels along a path. Useful for showcasing the "integration" features of a website.
Installation
Copy and paste the following code into your project:
vue
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, useId, watch } from "vue";
const props = withDefaults(
defineProps<{
containerRef: any;
fromRef: any;
toRef: any;
className?: string;
curvature?: number;
reverse?: boolean;
pathColor?: string;
pathWidth?: number;
pathOpacity?: number;
gradientStartColor?: string;
gradientStopColor?: string;
delay?: number;
duration?: number;
startXOffset?: number;
startYOffset?: number;
endXOffset?: number;
endYOffset?: number;
width?: number;
height?: number;
}>(),
{
curvature: 0,
reverse: false,
duration: Math.random() * 3 + 4,
delay: 3,
pathColor: "gray",
pathWidth: 2,
pathOpacity: 0.2,
gradientStartColor: "#ffaa40",
gradientStopColor: "#9c40ff",
startXOffset: 0,
startYOffset: 0,
endXOffset: 0,
endYOffset: 0,
},
);
const id = `pattern-${useId()}`;
// Keyframe values for the animated gradient. We animate the gradient's own
// x1/x2 attributes via native SVG (SMIL) `<animate>` elements instead of a CSS
// transform. WebKit (Safari and Chrome on iOS) does not move an SVG gradient
// via CSS transforms, which is why the beam was invisible there.
const gradientCoordinates = computed(() =>
props.reverse ? { x1: "90%;-10%", x2: "100%;0%" } : { x1: "10%;110%", x2: "0%;100%" },
);
const svgDimensions = ref({ width: 0, height: 0 });
const pathD = ref("");
function updatePath() {
if (props.containerRef && props.fromRef?.circleRef && props.toRef?.circleRef) {
const containerRect = props.containerRef?.getBoundingClientRect();
const rectA = props.fromRef?.circleRef?.getBoundingClientRect();
const rectB = props.toRef?.circleRef?.getBoundingClientRect();
const svgWidth = containerRect?.width;
const svgHeight = containerRect?.height;
svgDimensions.value.width = svgWidth;
svgDimensions.value.height = svgHeight;
const startX = rectA.left - containerRect.left + rectA.width / 2 + props.startXOffset;
const startY = rectA.top - containerRect.top + rectA.height / 2 + props.startYOffset;
const endX = rectB.left - containerRect.left + rectB.width / 2 + props.endXOffset;
const endY = rectB.top - containerRect.top + rectB.height / 2 + props.endYOffset;
const controlY = startY - props.curvature;
const d = `M ${startX},${startY} Q ${(startX + endX) / 2},${controlY} ${endX},${endY}`;
pathD.value = d;
}
}
const controller = new AbortController();
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
window.addEventListener("resize", updatePath, {
signal: controller.signal,
});
// Observe the container so the path is recalculated once its layout settles.
// On iOS Safari/Chrome the container's dimensions are often 0 on the first
// tick, so relying on the window "resize" event alone leaves the SVG sized
// at 0x0 and nothing renders.
if (typeof ResizeObserver !== "undefined" && props.containerRef) {
resizeObserver = new ResizeObserver(() => updatePath());
resizeObserver.observe(props.containerRef);
}
// Compute the initial path once the DOM has been laid out.
nextTick(updatePath);
});
onUnmounted(() => {
controller.abort();
resizeObserver?.disconnect();
});
watch(
props,
(_) => {
updatePath();
},
{ deep: true },
);
</script>
<template>
<svg
:width="svgDimensions?.width"
:height="svgDimensions?.height"
xmlns="http://www.w3.org/2000/svg"
class="pointer-events-none absolute left-0 top-0 transform-gpu stroke-2"
:class="[props.className]"
:viewBox="`0 0 ${svgDimensions?.width} ${svgDimensions?.height}`"
>
<path
:d="pathD"
:stroke="pathColor"
fill="none"
:stroke-width="pathWidth"
:stroke-opacity="pathOpacity"
stroke-linecap="round"
/>
<path :d="pathD" :stroke="`url(#${id})`" fill="none" stroke-linecap="round" />
<defs>
<linearGradient :id="id" gradientUnits="userSpaceOnUse" x1="0%" x2="0%" y1="0%" y2="0%">
<animate
attributeName="x1"
:values="gradientCoordinates.x1"
:dur="`${duration}s`"
:begin="`${delay}s`"
repeatCount="indefinite"
calcMode="spline"
keyTimes="0;1"
keySplines="0.16 1 0.3 1"
/>
<animate
attributeName="x2"
:values="gradientCoordinates.x2"
:dur="`${duration}s`"
:begin="`${delay}s`"
repeatCount="indefinite"
calcMode="spline"
keyTimes="0;1"
keySplines="0.16 1 0.3 1"
/>
<stop :stop-color="props.gradientStartColor" stop-opacity="0" />
<stop :stop-color="props.gradientStartColor" />
<stop offset="32.5%" :stop-color="props.gradientStopColor" />
<stop offset="100%" :stop-color="props.gradientStopColor" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
</template>vue
<script setup lang="ts">
import { ref } from "vue";
const circleRef = ref();
defineExpose({
circleRef,
});
</script>
<template>
<div
ref="circleRef"
class="z-10 flex h-12 w-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]"
>
<slot />
</div>
</template>Examples
Animated Beam Uni-Directional
Animated Beam Bi-Directional
Animated Beam Multiple Inputs
Animated Beam Multiple Outputs
Props
Animated Beam
| Prop | Type | Description | Default |
|---|---|---|---|
| class | string | The class for the component. | - |
| containerRef | ref | The container ref. | - |
| fromRef | ref | The ref of the element from which the beam should start. | - |
| toRef | ref | The ref of the element to which the beam should end. | - |
| curvature | number | The curvature of the beam. | 0 |
| reverse | boolean | Whether the beam should be reversed. | false |
| duration | number | The duration of the beam. | 5 |
| delay | number | The delay of the beam. | 0 |
| pathColor | string | The color of the beam. | "gray" |
| pathWidth | number | The width of the beam. | 2 |
| pathOpacity | number | The opacity of the beam. | 0.2 |
| gradientStartColor | string | The start color of the gradient. | "#ffaa40" |
| gradientStopColor | string | The stop color of the gradient. | "#9c40ff" |
| startXOffset | number | The start x offset of the beam. | 0 |
| startYOffset | number | The start y offset of the beam. | 0 |
| endXOffset | number | The end x offset of the beam. | 0 |
| endYOffset | number | The end y offset of the beam. | 0 |