Studio KitHigh-End Animated Cardsmotion/react

DrawBorderCard

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 props
0.8
2
16

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add draw-border-card

Writes 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.

example.tsx
1import { DrawBorderCard } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <DrawBorderCard />6  );7}

Props

PropTypeDefault
title

Card heading (ignored when children are provided).

string'Draw Border Card'
body

Card body copy (ignored when children are provided).

string'Hover or focus the card and a single stroke traces its outline, matched exactly to the corner radius.'
children

Custom card content.

React.ReactNodeundefined
duration

Stroke draw duration in seconds.

number0.8
strokeColor

Stroke color.

string'#38bdf8'
strokeWidth

Stroke width in pixels.

number2
radius

Corner radius in pixels, shared by the card and the stroke.

number16
className

Extra class merged onto the root.

string''

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

components/ui/draw-border-card.tsx
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}