Chrome
FreeMoving Border
The universal sign for selected, drop here and being edited, drawn at the exact size and radius.
Install
No key needed.
npx @galactrex/ui add moving-borderUsage
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.
"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
| Prop | Type | Default | Description |
|---|---|---|---|
dash / gap | number | 8 / 6 | Dash and gap in px. |
width | number | 1.5 | Stroke in px. |
radius | number | 12 | Match the parent. |
speed | number | 24 | px per second; negative reverses. |
color | string | — | Any CSS colour; defaults to a theme token. |
className | string | — | 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.