Selection Presence

Dashed-border overlay marking what another user has selected on the canvas.

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/selection-presence.json

Storybook

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

View in Storybook

4 stories available:

Code

"use client"; import { type ComponentPropsWithoutRef, forwardRef, type ReactNode, } from "react"; import { cn } from "../../lib/utils"; /** * Localizable strings. * * @public */ export type SelectionPresenceLabels = { /** Aria-label override. Defaults to `"Selection presence"`. */ region?: string; }; const DEFAULT_LABELS = { region: "Selection presence", } as const satisfies Required<SelectionPresenceLabels>; /** * Props for {@link SelectionPresence}. * * @public */ export type SelectionPresenceProps = { /** Tailwind / CSS color used for the border + chip. Defaults to `var(--foreground)`. */ color?: string; /** Selection rectangle height in pixels. */ height: number; /** Localizable strings. */ labels?: SelectionPresenceLabels; /** Optional name shown in the corner chip (e.g. owner of the selection). */ name?: ReactNode; /** Selection rectangle width in pixels. */ width: number; /** Selection rectangle X (top-left) in canvas pixels. */ x: number; /** Selection rectangle Y (top-left) in canvas pixels. */ y: number; } & ComponentPropsWithoutRef<"div">; /** * Overlay marking what another user has selected on the canvas. The * dashed border + soft fill stay calm so they communicate * "someone-else's-selection" without blocking primary content. Pure * presentation; the host computes the rect from the remote user's * viewport and supplies an accent color per user. * * The wrapper is `pointer-events: none` so host gestures pass through. * * @example * ```tsx * <div className="relative h-screen w-screen"> * <Canvas /> * <SelectionPresence * x={120} y={80} width={240} height={120} * color="#5b8def" * name="Bea" * /> * </div> * ``` * * @public */ export const SelectionPresence = forwardRef< HTMLDivElement, SelectionPresenceProps >((props, ref) => { const { className, color, height, labels, name, width, x, y, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; const accent = color ?? "var(--foreground)"; const ariaLabel = typeof name === "string" ? `${resolvedLabels.region}: ${name}` : resolvedLabels.region; return ( <div aria-label={ariaLabel} className={cn( "pointer-events-none absolute z-20 rounded-md border-2 border-dashed", className, )} data-selection-presence ref={ref} role="img" style={{ backgroundColor: `color-mix(in srgb, ${accent} 10%, transparent)`, borderColor: accent, height, left: x, top: y, width, }} {...rest} > {name === null || name === undefined ? null : ( <span className="absolute -top-5 left-0 inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium text-white shadow-sm" data-selection-presence-chip style={{ backgroundColor: accent }} > {name} </span> )} </div> ); }); SelectionPresence.displayName = "SelectionPresence";

Dependencies

  • @vllnt/ui@^0.2.1