Outline button whose solid fill slides in from a chosen edge on hover or keyboard focus while the label inverts color, with configurable fill color and timing.
Preview
Customize
6 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add slide-fill-buttonWrites components/ui/slide-fill-button.tsx and components/styles/slide-fill-button.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { SlideFillButton } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <SlideFillButton />6 );7}Props
Accessibility
Semantic <button type="button">; the fill layer is aria-hidden and decorative. The fill also plays on :focus-visible keyboard focus (not on mouse focus), touch taps do not leave a sticky fill, and prefers-reduced-motion makes the fill and color change instant.
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/slide-fill-button.module.css";6 7export interface SlideFillButtonProps {8 /** Button label (ignored when children are provided) */9 label?: string;10 /** Custom button content */11 children?: React.ReactNode;12 /** Edge the fill slides in from */13 direction?: "left" | "right" | "up" | "down";14 /** Fill color */15 fillColor?: string;16 /** Label color while filled */17 fillTextColor?: string;18 /** Fill transition duration in seconds */19 duration?: number;20 /** Disable the button */21 disabled?: boolean;22 /** Click handler */23 onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;24 /** Optional className */25 className?: string;26}27 28const OFFSETS: Record<NonNullable<SlideFillButtonProps["direction"]>, { x: string; y: string }> = {29 left: { x: "-101%", y: "0%" },30 right: { x: "101%", y: "0%" },31 up: { x: "0%", y: "101%" },32 down: { x: "0%", y: "-101%" },33};34 35export function SlideFillButton({36 label = "Get started",37 children,38 direction = "left",39 fillColor = "#f4f4f6",40 fillTextColor = "#09090b",41 duration = 0.35,42 disabled = false,43 onClick,44 className = "",45}: SlideFillButtonProps) {46 const prefersReducedMotion = useReducedMotion();47 const [hovered, setHovered] = React.useState(false);48 const [focused, setFocused] = React.useState(false);49 const active = !disabled && (hovered || focused);50 const from = OFFSETS[direction] ?? OFFSETS.left;51 52 const handleFocus = (event: React.FocusEvent<HTMLButtonElement>) => {53 let visible = true;54 try {55 visible = event.currentTarget.matches(":focus-visible");56 } catch {57 visible = true;58 }59 setFocused(visible);60 };61 62 const transition = prefersReducedMotion63 ? { duration: 0 }64 : { duration, ease: [0.22, 1, 0.36, 1] as const };65 66 return (67 <button68 type="button"69 disabled={disabled}70 onClick={onClick}71 onPointerEnter={(e) => e.pointerType !== "touch" && setHovered(true)}72 onPointerLeave={() => setHovered(false)}73 onFocus={handleFocus}74 onBlur={() => setFocused(false)}75 data-active={active || undefined}76 style={77 {78 "--mf-sf-fill": fillColor,79 "--mf-sf-fill-text": fillTextColor,80 "--mf-sf-duration": `${prefersReducedMotion ? 0 : duration}s`,81 } as React.CSSProperties82 }83 className={`${styles.root} ${className}`.trim()}84 >85 <motion.span86 aria-hidden="true"87 className={styles.fill}88 initial={false}89 animate={active ? { x: "0%", y: "0%" } : from}90 transition={transition}91 />92 <span className={styles.label}>{children ?? label}</span>93 </button>94 );95}