1import { useMeasure } from "@uidotdev/usehooks";2
3import { AnchorGrid } from "@/components/ui/pxl/anchor-grid";4
5export default function AnchorGridDemo() {6 const [ref, { width, height }] = useMeasure();7
8 return (9 <div className="w-full max-w-md aspect-video" ref={ref}>10 <AnchorGrid11 containerWidth={width ?? 0}12 containerHeight={height ?? 0}13 cols={9}14 rows={5}15 gap={12}16 showGridLines17 widgets={[18 {19 id: "widget_1",20 x: 0,21 y: 0,22 w: 3,23 h: 1,24 anchorX: "left",25 anchorY: "top",26 render() {27 return <div className="size-full pixel-border pixel-color-danger-foreground p-(--pixel-size)"><div className="size-full bg-danger"></div></div>;28 },29 },30 {31 id: "widget_2",32 x: 1,33 y: 1,34 w: 2,35 h: 2,36 anchorX: "left",37 anchorY: "top",38 render() {39 return <div className="size-full pixel-border pixel-color-success-foreground p-(--pixel-size)"><div className="size-full bg-success"></div></div>;40 },41 },42 {43 id: "widget_3",44 x: 4,45 y: 1,46 w: 4,47 h: 1,48 anchorX: "left",49 anchorY: "top",50 render() {51 return <div className="size-full pixel-border pixel-color-primary-foreground p-(--pixel-size)"><div className="size-full bg-primary"></div></div>;52 },53 },54 {55 id: "widget_4",56 x: 5,57 y: 3,58 w: 1,59 h: 1,60 anchorX: "left",61 anchorY: "top",62 render() {63 return <div className="size-full pixel-border pixel-color-info-foreground p-(--pixel-size)"><div className="size-full bg-info"></div></div>;64 },65 },66 ]}67 />68 </div>69 );70}Installation
Section titled “Installation”pnpm dlx shadcn@latest add pxl-ui/registry/anchor-grid
Copy and paste the following code into your project.
1import { type ComponentType, useMemo } from "react";2
3import { cn } from "@/lib/utils";4
5export type Widget = {6 id: string;7 x: number;8 y: number;9 w: number;10 h: number;11 anchorX: "left" | "right";12 anchorY: "top" | "bottom";13 className?: string;14 render: ComponentType;15};16
17export function computeCellSize(18 containerWidth: number,19 containerHeight: number,20 cols: number,21 rows: number,22 gap: number,23) {24 const sizeByWidth = (containerWidth - gap * (cols - 1)) / cols;25 const sizeByHeight = (containerHeight - gap * (rows - 1)) / rows;26 return Math.max(0, Math.floor(Math.min(sizeByWidth, sizeByHeight)));27}28
29export function resolveWidgetPlacement(30 { x, y, w = 1, h = 1, anchorX = "left", anchorY = "top" }: Widget,31 cols: number,32 rows: number,33) {34 const colStart = anchorX === "right" ? cols - x - w + 1 : x + 1;35 const rowStart = anchorY === "bottom" ? rows - y - h + 1 : y + 1;36 return {37 gridColumn: `${colStart} / ${colStart + w}`,38 gridRow: `${rowStart} / ${rowStart + h}`,39 };40}41
42type Props = {43 containerHeight: number;44 containerWidth: number;45 cols: number;46 rows: number;47 gap?: number;48 gridAnchorX?: "center" | "left" | "right";49 gridAnchorY?: "center" | "top" | "bottom";50 widgets: Widget[];51 showGridLines?: boolean;52};53
54export function AnchorGrid({55 containerWidth,56 containerHeight,57 cols,58 rows,59 gap = 12,60 gridAnchorX = "left",61 gridAnchorY = "top",62 widgets = [],63 showGridLines = false,64}: Props) {65 const cellSize = useMemo(66 () => computeCellSize(containerWidth, containerHeight, cols, rows, gap),67 [containerWidth, containerHeight, cols, rows, gap]68 );69
70 const gridPixelWidth = cellSize * cols + gap * (cols - 1);71 const gridPixelHeight = cellSize * rows + gap * (rows - 1);72
73 const justifyMap = {74 left: "flex-start",75 center: "center",76 right: "flex-end",77 };78 const alignMap = { top: "flex-start", center: "center", bottom: "flex-end" };79
80 return (81 <div82 className={"flex relative overflow-hidden"}83 style={{84 width: containerWidth,85 height: containerHeight,86 justifyContent: justifyMap[gridAnchorX],87 alignItems: alignMap[gridAnchorY],88 }}89 >90 <div91 className="relative"92 style={{93 border: showGridLines ? "1px solid var(--border)" : "none",94 width: gridPixelWidth,95 height: gridPixelHeight,96 display: "grid",97 gridTemplateColumns: `repeat(${cols}, ${cellSize}px)`,98 gridTemplateRows: `repeat(${rows}, ${cellSize}px)`,99 gap: `${gap}px`,100 backgroundImage: showGridLines101 ? `102 linear-gradient(103 to right,104 var(--border) 1px,105 transparent 1px106 ),107 linear-gradient(108 to bottom,109 var(--border) 1px,110 transparent 1px111 )112 `113 : "none",114
115 backgroundSize: `${cellSize + gap}px ${cellSize + gap}px`,116 }}117 >118 {widgets.map((widget) => {119 const placement = resolveWidgetPlacement(widget, cols, rows);120 const Render = widget.render;121
122 return (123 <div124 key={widget.id}125 className={cn(126 "group/widget @container/widget flex w-full h-full items-center justify-center overflow-hidden",127 widget.className,128 )}129 style={placement}130 >131 <Render />132 </div>133 );134 })}135 </div>136 </div>137 );138}Update the import paths to match your project setup.
import { AnchorGrid } from "@/components/ui/pxl/anchor-grid"<AnchorGrid containerWidth={500} containerHeight={500} cols={9} rows={5} gap={12} widgets={[ { id: "widget_1", x: 0, y: 0, w: 3, h: 1, anchorX: "left", anchorY: "top", render() { return <div className="size-full pixel-border pixel-color-danger-foreground p-(--pixel-size)"><div className="size-full bg-danger"></div></div>; }, }, ]} />Anchor Grid vs Widget Area
Section titled “Anchor Grid vs Widget Area”Use Anchor Grid to display your widgets in websites or other full screen based setups. For desktop/floating widgets, use Widget Area instead.