Transform Box Frame
Free Frames React component — TransformBoxFrame. MIT licensed, copy-paste ready for Next.js and Tailwind CSS.
Design-tool bounding box with separate line and handle colors — defaults to all black; pass lineColor and handleColor for dual-tone frames.
Preview
Setup
How to use
Free for personal and commercial use. No UI attribution required. Include the MIT copyright notice when copying component source into your project. Read the MIT license.
- 1
Run in your terminal:
$npm install clsx tailwind-merge lucide-react - 2
Copy
lib/cn.tsbelow. Skip if you already havecn(). - 3
Copy the code below and create
components/frames/transform-box-frame.tsxin your project. - 4
Import and render:
Exampleimport { TransformBoxFrame } from "@/components/frames/transform-box-frame";
lib/cn.ts
import { type ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
components/frames/transform-box-frame.tsx
import {forwardRef,type ComponentPropsWithoutRef,type CSSProperties,type ReactNode,} from "react";import { cn } from "@/lib/cn";const DEFAULT_LINE_COLOR = "#171717";const DEFAULT_HANDLE_COLOR = "#171717";function resolveFrameColors({color,lineColor,handleColor,}: Readonly<{color?: string;lineColor?: string;handleColor?: string;}>) {const shared = color ?? DEFAULT_LINE_COLOR;return {line: lineColor ?? shared,handle: handleColor ?? color ?? DEFAULT_HANDLE_COLOR,};}const MEDIA_SLOT ="[&_img]:block [&_img]:size-full [&_img]:object-cover [&_video]:block [&_video]:size-full [&_video]:object-cover [&_iframe]:block [&_iframe]:size-full [&_iframe]:border-0";function toCssSize(value: string | number) {return typeof value === "number" ? `${value}px` : value;}function MediaSkeleton() {return (<div className="absolute inset-0 overflow-hidden bg-neutral-100"><div className="absolute inset-0 bg-neutral-200/45 motion-safe:animate-pulse" /><div className="absolute inset-0 flex items-center justify-center"><svgviewBox="0 0 48 48"aria-hiddenclassName="size-14 text-neutral-300"fill="none"stroke="currentColor"strokeWidth="1.5"><rect x="4" y="8" width="40" height="32" rx="2" /><circle cx="16" cy="18" r="3" fill="currentColor" stroke="none" /><pathd="M8 34l10-9 7 6 6-5 9 8"strokeLinecap="round"strokeLinejoin="round"/></svg></div></div>);}function MediaIcon({children,label,}: Readonly<{ children: ReactNode; label: string }>) {return (<div className="flex flex-col items-center gap-1.5"><div className="flex size-9 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-500">{children}</div><span className="font-mono text-[9px] text-neutral-400">{label}</span></div>);}function MediaPlaceholder() {return (<div className="flex size-full flex-col items-center justify-center gap-3 bg-neutral-50 p-4"><div className="flex items-center gap-4"><MediaIcon label="image"><svgviewBox="0 0 20 20"aria-hiddenclassName="size-4"fill="none"stroke="currentColor"strokeWidth="1.5"><rect x="2.5" y="4" width="15" height="12" rx="1.5" /><circlecx="7"cy="8.5"r="1.25"fill="currentColor"stroke="none"/><pathd="M5.5 14l3.5-3 2.5 2 2-1.5 3 2.5"strokeLinecap="round"strokeLinejoin="round"/></svg></MediaIcon><MediaIcon label="video"><svgviewBox="0 0 20 20"aria-hiddenclassName="size-4"fill="none"stroke="currentColor"strokeWidth="1.5"><rect x="2.5" y="5" width="10" height="10" rx="1.5" /><path d="M12.5 8.5l4.5-2.5v8l-4.5-2.5z" strokeLinejoin="round" /></svg></MediaIcon><MediaIcon label="media"><svgviewBox="0 0 20 20"aria-hiddenclassName="size-4"fill="none"stroke="currentColor"strokeWidth="1.5"><rect x="3" y="3" width="14" height="14" rx="2" /><path d="M7 10h6M10 7v6" strokeLinecap="round" /></svg></MediaIcon></div><p className="font-sans text-[11px] text-neutral-500">Pass image, video, or media as children</p></div>);}type CornerHandleProps = Readonly<{handleColor: string;className?: string;}>;function CornerHandle({ className, handleColor }: CornerHandleProps) {return (<spanaria-hiddenstyle={{ borderColor: handleColor }}className={cn("pointer-events-none absolute z-20 box-border size-3.5 border bg-white shadow-[0_1px_0_rgba(0,0,0,0.05)]",className,)}/>);}type FrameEdgeProps = Readonly<{lineColor: string;className?: string;}>;function FrameEdge({ className, lineColor }: FrameEdgeProps) {return (<spanaria-hiddenstyle={{ borderColor: lineColor }}className={cn("pointer-events-none absolute z-10", className)}/>);}export type TransformBoxFrameProps = Readonly<{children?: ReactNode;width?: string | number;height?: string | number;color?: string;lineColor?: string;handleColor?: string;mediaClassName?: string;label?: string;skeleton?: boolean;} & ComponentPropsWithoutRef<"div">>;export const TransformBoxFrame = forwardRef<HTMLDivElement,TransformBoxFrameProps>(({className,children,width,height,color,lineColor,handleColor,mediaClassName,label,skeleton = false,style,...props},ref,) => {const hasFixedHeight = height !== undefined;const colors = resolveFrameColors({ color, lineColor, handleColor });const frameStyle: CSSProperties = {...(width !== undefined ? { width: toCssSize(width) } : undefined),...(height !== undefined ? { height: toCssSize(height) } : undefined),...style,};return (<divref={ref}data-slot="transform-box-frame"className={cn("font-sans",width === undefined && "w-full max-w-xl",className,)}style={frameStyle}{...props}><div className={cn("relative p-1.75", hasFixedHeight && "h-full")}><divclassName={cn("relative z-0 min-h-0 overflow-hidden bg-neutral-50",hasFixedHeight ? "h-full" : (mediaClassName ?? "aspect-video"),)}><div className={cn("absolute inset-0", MEDIA_SLOT)}>{skeleton ? (<MediaSkeleton />) : ((children ?? <MediaPlaceholder />))}</div></div><FrameEdgelineColor={colors.line}className="top-1.75 right-1.75 left-1.75 border-t"/><FrameEdgelineColor={colors.line}className="top-1.75 right-1.75 bottom-1.75 border-r"/><FrameEdgelineColor={colors.line}className="right-1.75 bottom-1.75 left-1.75 border-b"/><FrameEdgelineColor={colors.line}className="top-1.75 bottom-1.75 left-1.75 border-l"/><CornerHandle handleColor={colors.handle} className="top-0 left-0" /><CornerHandle handleColor={colors.handle} className="top-0 right-0" /><CornerHandlehandleColor={colors.handle}className="bottom-0 left-0"/><CornerHandlehandleColor={colors.handle}className="right-0 bottom-0"/></div>{label ? (<p className="mt-2 text-center font-mono text-[10px] tracking-[0.16em] text-neutral-400 uppercase">{label}</p>) : null}</div>);},);TransformBoxFrame.displayName = "TransformBoxFrame";