Snap Guides

Alignment guide overlay — dashed vertical and horizontal lines that surface during a drag.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/snap-guides.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

3 stories available:

Code

"use client"; import type { ComponentPropsWithoutRef } from "react"; import { cn } from "../../lib/utils"; /** * One alignment line. * * @public */ export type SnapGuide = | { /** Stable id (used as React key). */ id: string; /** Horizontal guide — runs left to right at this `y` in container px. */ orientation: "horizontal"; y: number; } | { /** Stable id (used as React key). */ id: string; /** Vertical guide — runs top to bottom at this `x` in container px. */ orientation: "vertical"; x: number; }; /** * Localizable strings. * * @public */ export type SnapGuidesLabels = { /** Aria-label for the layer. Defaults to `"Snap guides"`. */ region?: string; }; const DEFAULT_LABELS = { region: "Snap guides", } as const satisfies Required<SnapGuidesLabels>; /** * Props for {@link SnapGuides}. * * @public */ export type SnapGuidesProps = { /** Active alignment lines. Pass an empty array to hide all. */ guides: SnapGuide[]; /** Localizable strings. */ labels?: SnapGuidesLabels; } & ComponentPropsWithoutRef<"div">; /** * Alignment guide overlay for a canvas. Renders dashed lines at the * `x` / `y` coordinates supplied by the host snapper. Pure * presentation — the host computes which guides are active during a * drag and unmounts them on release. * * @example * ```tsx * <SnapGuides * guides={[ * { id: "x-200", orientation: "vertical", x: 200 }, * { id: "y-160", orientation: "horizontal", y: 160 }, * ]} * /> * ``` * * @public */ export const SnapGuides = ({ ref, ...props }: SnapGuidesProps & { ref?: React.Ref<HTMLDivElement> }) => { const { className, guides, labels, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; return ( <div aria-label={resolvedLabels.region} className={cn("pointer-events-none absolute inset-0 z-30", className)} data-snap-guide-count={guides.length} ref={ref} {...rest} > {guides.map((guide) => { const isVertical = guide.orientation === "vertical"; const offset = isVertical ? guide.x : guide.y; return ( <span aria-hidden="true" className={cn( "absolute border-primary/70", isVertical ? "inset-y-0 w-px border-l border-dashed" : "inset-x-0 h-px border-t border-dashed", )} data-snap-guide-id={guide.id} data-snap-orientation={guide.orientation} key={guide.id} style={ isVertical ? { left: `${offset.toString()}px` } : { top: `${offset.toString()}px` } } /> ); })} </div> ); }; SnapGuides.displayName = "SnapGuides";

Dependencies

  • @vllnt/ui@^0.3.0