Skip to main content
Back to discovery

Live preview recorded from Dqnamo Kitchen; component and provider source published by JP / dqnamo. Preview: platform recorded · rights authorized.

Dynamic Button

A button that smoothly resizes as its label animates.

dSource Landdqnamo

Why it stands out

A single action button transitions through idle, working, and completed labels. Its pill-shaped frame smoothly resizes around each label so loading feedback does not jump or disturb the surrounding layout.

Prompt

import {
  AnimatePresence,
  type HTMLMotionProps,
  motion,
  useReducedMotion,
} from "motion/react";
import {
  forwardRef,
  type Key,
  type ReactNode,
  useCallback,
  useImperativeHandle,
  useLayoutEffect,
  useRef,
  useState,
	} from "react";
	
	type DynamicButtonVariant = "primary" | "secondary";
	
	const uiEaseOut = [0.23, 1, 0.32, 1] as const;
	
	export type DynamicButtonProps = Omit<
	  HTMLMotionProps<"button">,
  "animate" | "children" | "initial" | "ref" | "transition"
> & {
  children: string;
  icon?: ReactNode;
  stateKey?: Key;
  variant?: DynamicButtonVariant;
  width?: "content" | "full";
};

const buttonClasses: Record<DynamicButtonVariant, string> = {
  primary: "border-neutral-950 bg-neutral-950 text-white hover:bg-neutral-800",
  secondary:
    "border-neutral-200 bg-white text-neutral-950 hover:border-neutral-300 hover:bg-neutral-50",
};

function getButtonClassName({
  className,
  variant = "primary",
}: {
  className?: string;
  variant?: DynamicButtonVariant;
}) {
  return [
    "inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-lg border px-2 text-sm font-medium transition-colors",
    buttonClasses[variant],
    className,
  ]
    .filter(Boolean)
    .join(" ");
}

export const DynamicButton = forwardRef<HTMLButtonElement, DynamicButtonProps>(
  function DynamicButton(
    {
      children,
      className,
      icon,
      stateKey,
      type = "button",
      variant = "primary",
      width = "content",
      ...props
    },
    forwardedRef,
  ) {
    const shouldReduceMotion = useReducedMotion();
    const buttonRef = useRef<HTMLButtonElement>(null);
    const measureRef = useRef<HTMLSpanElement>(null);
    const measurementSignatureRef = useRef("");
    const [measuredWidth, setMeasuredWidth] = useState<number | null>(null);
    const iconKey = stateKey ?? children;
    const measurementSignature = [
      children,
      className,
      icon ? "icon" : "no-icon",
      stateKey,
      variant,
    ].join("\0");
	    const shouldMeasureWidth = width === "content";
	    const widthTransition = shouldReduceMotion
	      ? { duration: 0 }
	      : { bounce: 0, duration: 0.26, type: "spring" as const };
	    const contentTransition = {
	      duration: shouldReduceMotion ? 0.16 : 0.18,
	      ease: uiEaseOut,
	    };
	    const contentVisible = { opacity: 1, transform: "translateY(0px)" };
	    const contentInitial = shouldReduceMotion
	      ? { opacity: 0, transform: "translateY(0px)" }
	      : { opacity: 0, transform: "translateY(8px)" };
	    const contentExit = shouldReduceMotion
	      ? { opacity: 0, transform: "translateY(0px)" }
	      : { opacity: 0, transform: "translateY(-8px)" };

    useImperativeHandle(
      forwardedRef,
      () => buttonRef.current as HTMLButtonElement,
    );

    const syncWidth = useCallback(() => {
      const button = buttonRef.current;
      const measure = measureRef.current;

      if (!button || !measure || !shouldMeasureWidth) {
        return;
      }

      const styles = window.getComputedStyle(button);
      const horizontalPadding =
        Number.parseFloat(styles.paddingLeft) +
        Number.parseFloat(styles.paddingRight) +
        Number.parseFloat(styles.borderLeftWidth) +
        Number.parseFloat(styles.borderRightWidth);
      const nextWidth = Math.ceil(measure.scrollWidth + horizontalPadding);

      setMeasuredWidth((currentWidth) =>
        currentWidth === nextWidth ? currentWidth : nextWidth,
      );
    }, [shouldMeasureWidth]);

    useLayoutEffect(() => {
      const measure = measureRef.current;

      if (!measure || !shouldMeasureWidth) {
        setMeasuredWidth(null);
        return;
      }

      syncWidth();

      const observer = new ResizeObserver(syncWidth);
      observer.observe(measure);
      window.addEventListener("resize", syncWidth);

      return () => {
        observer.disconnect();
        window.removeEventListener("resize", syncWidth);
      };
    }, [shouldMeasureWidth, syncWidth]);

    useLayoutEffect(() => {
      if (measurementSignatureRef.current === measurementSignature) {
        return;
      }

      measurementSignatureRef.current = measurementSignature;
      syncWidth();
    }, [measurementSignature, syncWidth]);

    return (
      <motion.button
        {...props}
        animate={
          width === "content" ? { width: measuredWidth ?? "auto" } : undefined
        }
	        className={getButtonClassName({
	          className: [
	            "relative overflow-hidden whitespace-nowrap",
	            "transition-[color,background-color,border-color,box-shadow,transform] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]",
	            width === "full" ? "w-full" : "",
	            className,
	          ]
            .filter(Boolean)
            .join(" "),
          variant,
	        })}
	        initial={false}
	        ref={buttonRef}
	        transition={width === "content" ? { width: widthTransition } : undefined}
	        type={type}
	      >
        <span className="relative inline-flex items-center gap-1.5">
          {icon ? (
            <span className="relative inline-grid size-[15px] shrink-0 overflow-hidden">
	              <AnimatePresence initial={false} mode="popLayout">
	                <motion.span
	                  animate={contentVisible}
	                  className="col-start-1 row-start-1 flex size-[15px] items-center justify-center"
	                  exit={contentExit}
	                  initial={contentInitial}
	                  key={iconKey}
	                  transition={contentTransition}
	                >
                  {icon}
                </motion.span>
              </AnimatePresence>
            </span>
          ) : null}

          <span className="relative inline-grid overflow-hidden">
	            <AnimatePresence initial={false} mode="popLayout">
	              <motion.span
	                animate={contentVisible}
	                className="col-start-1 row-start-1 block"
	                exit={contentExit}
	                initial={contentInitial}
	                key={children}
	                transition={contentTransition}
	              >
                {children}
              </motion.span>
            </AnimatePresence>
          </span>
        </span>
        <span
          aria-hidden="true"
          className="pointer-events-none absolute inline-flex items-center gap-1.5 opacity-0"
          ref={measureRef}
        >
          {icon ? (
            <span className="flex size-[15px] shrink-0 items-center justify-center">
              {icon}
            </span>
          ) : null}
          <span>{children}</span>
        </span>
      </motion.button>
    );
  },
);

Discovery vocabulary

Related by governed terms

Continue comparing

More from dqnamo