Card that traces its own rounded outline with a single SVG stroke on hover or focus, using a normalised pathLength so the stroke matches the card radius at any size.
Preview
Draw Border Card
Hover or focus the card and a single stroke traces its outline, matched exactly to the corner radius.
Customize
6 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add draw-border-cardWrites components/ui/draw-border-card.tsx and components/styles/draw-border-card.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { DrawBorderCard } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <DrawBorderCard />6 );7}Props
Accessibility
Root is a focusable <article>; the stroke draws on hover and on focus-within so keyboard users get the same affordance, and the SVG outline is aria-hidden. Under prefers-reduced-motion the full outline fades in instead of drawing.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/draw-border-card.module.css";6 7export interface DrawBorderCardProps {8 /** Card heading (ignored when children are provided) */9 title?: string;10 /** Card body copy (ignored when children are provided) */11 body?: string;12 /** Custom card content */13 children?: React.ReactNode;14 /** Stroke draw duration in seconds */15 duration?: number;16 /** Stroke color */17 strokeColor?: string;18 /** Stroke width in pixels */19 strokeWidth?: number;20 /** Card corner radius in pixels (shared by the card and the stroke) */21 radius?: number;22 /** Optional className */23 className?: string;24}25 26export function DrawBorderCard({27 title = "Draw Border Card",28 body = "Hover or focus the card and a single stroke traces its outline, matched exactly to the corner radius.",29 children,30 duration = 0.8,31 strokeColor = "#38bdf8",32 strokeWidth = 2,33 radius = 16,34 className = "",35}: DrawBorderCardProps) {36 const prefersReducedMotion = useReducedMotion();37 const [hovered, setHovered] = React.useState(false);38 const [focused, setFocused] = React.useState(false);39 const active = hovered || focused;40 41 return (42 <article43 tabIndex={0}44 onPointerEnter={() => setHovered(true)}45 onPointerLeave={() => setHovered(false)}46 onFocus={() => setFocused(true)}47 onBlur={(e) => {48 if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocused(false);49 }}50 data-active={active || undefined}51 style={{ "--mf-db-radius": `${radius}px` } as React.CSSProperties}52 className={`${styles.root} ${className}`.trim()}53 >54 <svg className={styles.outline} aria-hidden="true">55 <motion.rect56 x="0"57 y="0"58 width="100%"59 height="100%"60 rx={radius}61 ry={radius}62 fill="none"63 stroke={strokeColor}64 strokeWidth={strokeWidth}65 strokeLinecap="round"66 initial={false}67 animate={68 prefersReducedMotion69 ? { pathLength: 1, opacity: active ? 1 : 0 }70 : { pathLength: active ? 1 : 0, opacity: active ? 1 : 0 }71 }72 transition={73 prefersReducedMotion74 ? { duration: 0.15 }75 : {76 pathLength: { duration, ease: [0.65, 0, 0.35, 1] },77 opacity: { duration: 0.12, delay: active ? 0 : duration * 0.6 },78 }79 }80 />81 </svg>82 <div className={styles.content}>83 {children ?? (84 <>85 <h3 className={styles.title}>{title}</h3>86 <p className={styles.body}>{body}</p>87 </>88 )}89 </div>90 </article>91 );92}