Components
Figma's vector-editing chrome rebuilt for the web, in two parts. FigmaFrame draws the 'selected layer' look around any element: a thin accent border that animates open, four corner handles that pop in one by one, and a live W×H dimension badge tracked with a ResizeObserver. VectorEditor is a live SVG bezier point editor: every anchor is a draggable dot and every bezier handle a draggable diamond joined by a thin tangent arm — drag anchors to move, drag handles to reshape, hold Alt to break a tangent into a corner, with Figma-style mirroring modes (none / angle / angle-length). Includes a lossless SVG path parser/serializer (M/L/H/V/C/S/Q/T/Z, multi-subpath with holes) so you can drop in any path, edit it, and read the new d string back. Zero dependencies, pure pointer math through the live screen CTM.

"use client";
import { useState } from "react";
import {
VectorEditor,
FigmaFrame,
parsePath,
type VectorPath,
type EditorStyle,
type FrameStyle,
} from "@/components/ui/vector-editor";
// The "arlan" wordmark, as an SVG path — the shape you drag the points on.
const WORD =
"M 19.8 150 L 51.8 150 L 62.2 118.2 L 115.8 118.2 L 126 150 L 158.8 150 L 104.6 7 L 73.6 7 L 19.8 150 Z M 69.6 94.6 L 88.8 34.6 L 89.2 34.6 L 108.6 94.6 L 69.6 94.6 Z M 195.8 100.4 C 195.8 81.8 208.8 71 224.6 71 C 226.8 71 228.6 71.2 231.2 71.6 L 231.2 47 C 227.8 46.4 225.2 46.4 222.2 46.4 C 210 46.4 201.2 54 196.2 65.4 L 195.8 65.4 L 195.8 47.8 L 168.4 47.8 L 168.4 150 L 195.8 150 L 195.8 100.4 Z M 242.6 150 L 270 150 L 270 7 L 242.6 7 L 242.6 150 Z M 317 153 C 332.4 153 344 146.6 351.2 136.8 L 351.6 136.8 C 353 146.4 358 151.2 369.6 151.2 C 373.8 151.2 379.2 150.8 384.4 149.8 L 384.4 135.6 C 383.2 135.8 382.6 135.8 382 135.8 C 378.4 135.8 377.8 133 377.8 126.8 L 377.8 84.2 C 377.8 54.8 361.8 44.4 336.6 44.4 C 303.2 44.4 289.4 60.8 288.4 80.4 L 315 80.4 C 316 69 321.4 63.8 335.4 63.8 C 346.8 63.8 351.2 68.4 351.2 74.8 C 351.2 84.8 337.4 86.6 324.6 89.2 C 301.6 93.8 284.2 101.6 284.2 123.4 C 284.2 141.2 296.8 153 317 153 Z M 327 133.4 C 317.4 133.4 312.4 129.2 312.4 120.8 C 312.4 112.2 317.8 107.2 331.8 104.2 C 341.2 102 347.4 100.4 351.2 98.4 L 351.2 109.2 C 351.2 123.2 341.6 133.4 327 133.4 Z M 466.4 88.6 L 466.4 150 L 494 150 L 494 80.2 C 494 56.8 479.8 44.6 460.4 44.6 C 442.4 44.6 432.2 53 426.8 61.8 L 426.4 61.8 L 426.4 47.8 L 399 47.8 L 399 150 L 426.4 150 L 426.4 89.6 C 426.4 74.8 434.8 67.2 448.8 67.2 C 460.8 67.2 466.4 73.8 466.4 88.6 Z";
const VIEW: [number, number, number, number] = [0, 0, 511, 169];
const settings = {
accent: "#0d99ff",
fill: "#0d99ff",
fillOpacity: 0.08,
strokeWidth: 1.5,
showRig: true,
showBadge: true,
};
export default function Demo(props: Partial<typeof settings>) {
const s = { ...settings, ...props };
const [path, setPath] = useState<VectorPath>(() => parsePath(WORD));
const editorStyle: EditorStyle = {
accent: s.accent,
arm: "#9bb7d4",
anchorR: 4,
handleR: 3.2,
pointFill: "#ffffff",
fill: s.fill,
fillOpacity: s.fillOpacity,
stroke: s.accent,
strokeWidth: s.strokeWidth,
showRig: s.showRig,
fillRule: "evenodd",
};
const frameStyle: FrameStyle = {
accent: s.accent,
handleSize: 8,
handleFill: "#ffffff",
borderWidth: 1,
showHandles: true,
showBadge: s.showBadge,
badgeBg: s.accent,
badgeText: "#ffffff",
};
return (
<div className="flex h-screen w-full items-center justify-center bg-background">
<FigmaFrame style={frameStyle} width={380} height={179}>
<div
className="flex items-center justify-center"
style={{ width: 380, height: 179 }}
>
<VectorEditor
path={path}
onChange={setPath}
style={editorStyle}
viewBox={VIEW}
width={340}
height={112}
/>
</div>
</FigmaFrame>
</div>
);
}