Account

Chrome

Free

Moving Border

The universal sign for selected, drop here and being edited, drawn at the exact size and radius.

Install

No key needed.

Terminal
npx @galactrex/ui add moving-border

Usage

Example
import { MovingBorder } from "@/components/galactrex/moving-border";

<div className="relative rounded-xl p-8"><MovingBorder />Drop files here</div>

Source

Installs to components/galactrex/moving-border.tsx, with the shared motion primitives beside it.

components/galactrex/moving-border.tsx
"use client";
import { useRef } from "react";
import { motion } from "motion/react";
import { cx } from "@/components/galactrex/motion/cx";
import { useElementSize } from "@/components/galactrex/motion/use-element-size";
import { useReducedMotion } from "@/components/galactrex/motion/use-reduced-motion";
import { useVisible } from "@/components/galactrex/motion/use-visible";

export type MovingBorderProps = {
  /** dash length in px */
  dash?: number;
  /** gap between dashes in px */
  gap?: number;
  /** stroke width in px */
  width?: number;
  /** corner radius in px; match the parent */
  radius?: number;
  /** px per second the dashes travel; negative runs the other way */
  speed?: number;
  color?: string;
  className?: string;
};

/** Marching ants: a dashed border whose dashes travel around the edge. The universal sign for "selected",
    "drop here" and "being edited". Place it inside a relative parent; it draws an SVG outline at the exact size
    and radius. Still with reduced motion; pauses offscreen. */
export function MovingBorder({ dash = 8, gap = 6, width = 1.5, radius = 12, speed = 24, color = "var(--gxui-ink)", className }: MovingBorderProps) {
  const ref = useRef<HTMLSpanElement>(null);
  const { width: w, height: h } = useElementSize(ref);
  const reduced = useReducedMotion();
  const visible = useVisible(ref);
  const period = dash + gap;
  return (
    <span ref={ref} aria-hidden className={cx("pointer-events-none absolute inset-0", className)}>
      {w > 0 && (
        <svg className="absolute inset-0 overflow-visible" width={w} height={h}>
          <motion.rect
            x={width / 2}
            y={width / 2}
            width={Math.max(0, w - width)}
            height={Math.max(0, h - width)}
            rx={Math.max(0, radius - width / 2)}
            fill="none"
            stroke={color}
            strokeWidth={width}
            strokeDasharray={`${dash} ${gap}`}
            animate={reduced || !visible || speed === 0 ? undefined : { strokeDashoffset: [0, speed > 0 ? -period : period] }}
            transition={{ duration: period / Math.max(1, Math.abs(speed)), ease: "linear", repeat: Infinity }}
          />
        </svg>
      )}
    </span>
  );
}

Props

PropTypeDefaultDescription
dash / gapnumber8 / 6Dash and gap in px.
widthnumber1.5Stroke in px.
radiusnumber12Match the parent.
speednumber24px per second; negative reverses.
colorstring—Any CSS colour; defaults to a theme token.
classNamestring—Extra classes for the outer element.

Variants

  • Drop zone. Around an upload area while dragging.
  • Selection. Around a selected layer.

Accessibility

  • Decorative; pair with a text state.

Motion and reduced motion

  • Dash offset loop on one SVG rect; pauses offscreen.
  • Reduced motion: still dashes.