A background hexagon pattern made with SVGs.
1"use client";2
3import { HexagonPattern } from "@/components/backgrounds/pxl/hexagon-pattern";4import { cn } from "@/lib/utils";5
6export default function HexagonPatternDemo() {7 return (8 <HexagonPattern9 hexagons={[10 [1, 1],11 [4, 4],12 [2, 2],13 [3, 4],14 [5, 4],15 [8, 2],16 [6, 3],17 [8, 5],18 [10, 10],19 ]}20 className={cn(21 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",22 "inset-0 skew-y-6",23 )}24 />25 );26}Installation
pnpm dlx shadcn@latest add pxl-ui/registry/backgrounds/hexagon-pattern
Copy and paste the following code into your project.
1import { cva, type VariantProps } from "class-variance-authority";2import { type SVGProps, useId } from "react";3
4import { cn } from "@/lib/utils";5
6type HexPoint = readonly [number, number];7
8const hexagonPatternVariants = cva(9 "pointer-events-none absolute inset-0 h-full w-full",10 {11 variants: {12 variant: {13 default: "bg-background fill-border stroke-border text-border",14 primary: "bg-primary fill-primary-foreground/30 stroke-primary-foreground/30 text-primary-foreground/30",15 secondary: "bg-secondary fill-secondary-foreground/30 stroke-secondary-foreground/30 text-secondary-foreground/30",16 muted: "bg-muted fill-muted-foreground/30 stroke-muted-foreground/30 text-muted-foreground/30",17 info: "bg-info fill-info-foreground/30 stroke-info-foreground/30 text-info-foreground/30",18 success: "bg-success fill-success-foreground/30 stroke-success-foreground/30 text-success-foreground/30",19 warning: "bg-warning fill-warning-foreground/30 stroke-warning-foreground/30 text-warning-foreground/30",20 danger: "bg-danger fill-danger-foreground/30 stroke-danger-foreground/30 text-danger-foreground/30",21 },22 },23 defaultVariants: {24 variant: "default",25 },26 },27);28
29function hexVertexList(30 cx: number,31 cy: number,32 r: number,33 direction: "horizontal" | "vertical",34): HexPoint[] {35 const startAngle = direction === "horizontal" ? 0 : 30;36 return Array.from({ length: 6 }, (_, i) => {37 const angle = ((startAngle + i * 60) * Math.PI) / 180;38 return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)] as const;39 });40}41
42function hexPoints(43 cx: number,44 cy: number,45 r: number,46 direction: "horizontal" | "vertical",47): string {48 return hexVertexList(cx, cy, r, direction)49 .map(([px, py]) => `${px},${py}`)50 .join(" ");51}52
53function edgeLexKey(a: HexPoint, b: HexPoint): string {54 const [p, q] =55 a[0] < b[0] || (a[0] === b[0] && a[1] <= b[1]) ? [a, b] : [b, a];56 return `${p[0].toFixed(6)},${p[1].toFixed(6)}|${q[0].toFixed(6)},${q[1].toFixed(6)}`;57}58
59function collectUniqueHexEdges(60 centers: [number, number][],61 r: number,62 direction: "horizontal" | "vertical",63): [HexPoint, HexPoint][] {64 const seen = new Set<string>();65 const edges: [HexPoint, HexPoint][] = [];66 for (const [cx, cy] of centers) {67 const verts = hexVertexList(cx, cy, r, direction);68 for (let i = 0; i < 6; i++) {69 const a = verts[i];70 const b = verts[(i + 1) % 6];71 const key = edgeLexKey(a, b);72 if (!seen.has(key)) {73 seen.add(key);74 edges.push([a, b]);75 }76 }77 }78 return edges;79}80
81function isSolidStrokeDasharray(strokeDasharray: string): boolean {82 const t = strokeDasharray.trim();83 return t === "" || t === "none" || t === "0";84}85
86function getHexSpacing(87 r: number,88 direction: "horizontal" | "vertical",89 gap: number,90): {91 colStep: number;92 rowStep: number;93 tileW: number;94 tileH: number;95} {96 const sqrt3 = Math.sqrt(3);97
98 // `gap` should match the visible edge-to-edge spacing, so we add it along99 // the shared-edge normal instead of directly on the raw x/y axes.100 if (direction === "horizontal") {101 const colStep = (3 * r) / 2 + (sqrt3 * gap) / 2;102 const rowStep = sqrt3 * r + gap;103
104 return {105 colStep,106 rowStep,107 tileW: colStep * 2,108 tileH: rowStep,109 };110 }111
112 const colStep = sqrt3 * r + gap;113 const rowStep = (3 * r) / 2 + (sqrt3 * gap) / 2;114
115 return {116 colStep,117 rowStep,118 tileW: colStep,119 tileH: rowStep * 2,120 };121}122
123function getTileGeometry(124 r: number,125 direction: "horizontal" | "vertical",126 gap: number,127): {128 tileW: number;129 tileH: number;130 centers: [number, number][];131} {132 if (direction === "horizontal") {133 const { colStep, rowStep, tileW, tileH } = getHexSpacing(r, direction, gap);134
135 const canonical: [number, number][] = [136 [colStep / 2, rowStep / 2],137 [(colStep * 3) / 2, rowStep],138 ];139
140 const centers: [number, number][] = [];141 for (const [cx, cy] of canonical) {142 centers.push([cx, cy]);143 if (cy - r < 0) centers.push([cx, cy + tileH]);144 if (cy + r > tileH) centers.push([cx, cy - tileH]);145 if (cx - r < 0) centers.push([cx + tileW, cy]);146 if (cx + r > tileW) centers.push([cx - tileW, cy]);147 if (cy - r < 0 && cx - r < 0) centers.push([cx + tileW, cy + tileH]);148 if (cy - r < 0 && cx + r > tileW) centers.push([cx - tileW, cy + tileH]);149 if (cy + r > tileH && cx - r < 0) centers.push([cx + tileW, cy - tileH]);150 if (cy + r > tileH && cx + r > tileW)151 centers.push([cx - tileW, cy - tileH]);152 }153
154 return { tileW, tileH, centers };155 } else {156 const { colStep, rowStep, tileW, tileH } = getHexSpacing(r, direction, gap);157
158 const canonical: [number, number][] = [159 [colStep / 2, rowStep / 2],160 [colStep, (rowStep * 3) / 2],161 ];162
163 const centers: [number, number][] = [];164 for (const [cx, cy] of canonical) {165 centers.push([cx, cy]);166 if (cy - r < 0) centers.push([cx, cy + tileH]);167 if (cy + r > tileH) centers.push([cx, cy - tileH]);168 if (cx - r < 0) centers.push([cx + tileW, cy]);169 if (cx + r > tileW) centers.push([cx - tileW, cy]);170 if (cy - r < 0 && cx - r < 0) centers.push([cx + tileW, cy + tileH]);171 if (cy - r < 0 && cx + r > tileW) centers.push([cx - tileW, cy + tileH]);172 if (cy + r > tileH && cx - r < 0) centers.push([cx + tileW, cy - tileH]);173 if (cy + r > tileH && cx + r > tileW)174 centers.push([cx - tileW, cy - tileH]);175 }176
177 return { tileW, tileH, centers };178 }179}180
181function hexCenter(182 col: number,183 row: number,184 r: number,185 direction: "horizontal" | "vertical",186 gap: number,187): [number, number] {188 if (direction === "horizontal") {189 const { colStep, rowStep } = getHexSpacing(r, direction, gap);190 const x = col * colStep + colStep / 2;191 const y = row * rowStep + rowStep / 2 + (col % 2 !== 0 ? rowStep / 2 : 0);192 return [x, y];193 } else {194 const { colStep, rowStep } = getHexSpacing(r, direction, gap);195 const x = col * colStep + colStep / 2 + (row % 2 !== 0 ? colStep / 2 : 0);196 const y = row * rowStep + rowStep / 2;197 return [x, y];198 }199}200
201function HexagonPattern({202 radius = 40,203 gap = 0,204 x = -1,205 y = -1,206 strokeDasharray = "0",207 direction = "horizontal",208 hexagons,209 className,210 variant = "default",211 ...props212}: SVGProps<SVGSVGElement> &213 VariantProps<typeof hexagonPatternVariants> & {214 /**215 * The radius of each hexagon (center to vertex).216 * @default 40217 */218 radius?: number;219 /**220 * Spacing in pixels between adjacent hexagons.221 * The tile grows by this amount while the visual radius stays fixed,222 * so the gap is evenly distributed on all sides of each hexagon.223 * @default 0224 */225 gap?: number;226 /**227 * Offset applied to the pattern origin on the x-axis.228 * @default -1229 */230 x?: number;231 /**232 * Offset applied to the pattern origin on the y-axis.233 * @default -1234 */235 y?: number;236 /**237 * Controls the orientation of the hexagons.238 * - `"horizontal"` — flat-top hexagons tiled in a horizontal honeycomb grid.239 * - `"vertical"` — pointy-top hexagons tiled in a vertical honeycomb grid.240 * @default "horizontal"241 */242 direction?: "horizontal" | "vertical";243 /**244 * SVG stroke-dasharray applied to each hexagon outline.245 * @default "0"246 */247 strokeDasharray?: string;248 /**249 * Array of [col, row] coordinates for hexagons that should be highlighted250 * (filled) on top of the repeating pattern — mirrors the `squares` prop of251 * GridPattern.252 */253 hexagons?: Array<[col: number, row: number]>;254 className?: string;255 [key: string]: unknown;256 }) {257 const id = useId();258
259 const { tileW, tileH, centers } = getTileGeometry(radius, direction, gap);260 const solidStroke = isSolidStrokeDasharray(strokeDasharray);261 const dashedEdges = solidStroke262 ? null263 : collectUniqueHexEdges(centers, radius, direction);264
265 return (266 <svg267 aria-hidden="true"268 className={cn(hexagonPatternVariants({ variant }), className)}269 {...props}270 >271 <defs>272 <pattern273 id={id}274 width={tileW}275 height={tileH}276 patternUnits="userSpaceOnUse"277 x={x}278 y={y}279 >280 {solidStroke281 ? centers.map(([cx, cy]) => (282 <polygon283 className="fill-none"284 key={`${cx}-${cy}`}285 points={hexPoints(cx, cy, radius, direction)}286 strokeDasharray={strokeDasharray}287 />288 ))289 : dashedEdges?.map(([a, b]) => (290 <line291 className="fill-none"292 key={edgeLexKey(a, b)}293 x1={a[0]}294 x2={b[0]}295 y1={a[1]}296 y2={b[1]}297 strokeDasharray={strokeDasharray}298 />299 ))}300 </pattern>301 </defs>302
303 <rect width="100%" height="100%" fill={`url(#${id})`} stroke="none" />304
305 {hexagons && hexagons.length > 0 && (306 <svg aria-hidden="true" className="overflow-visible" x={x} y={y}>307 {hexagons.map(([col, row]) => {308 const [cx, cy] = hexCenter(col, row, radius, direction, gap);309 return (310 <polygon311 key={`${col}-${row}`}312 points={hexPoints(cx, cy, radius - 1, direction)}313 strokeWidth="0"314 />315 );316 })}317 </svg>318 )}319 </svg>320 );321}322
323export { HexagonPattern };Update the import paths to match your project setup.
import { HexagonPattern } from "@/components/backgrounds/pxl/hexagon-pattern"<HexagonPattern />Linear Gradient
1"use client";2
3import { HexagonPattern } from "@/components/backgrounds/pxl/hexagon-pattern";4import { cn } from "@/lib/utils";5
6export default function HexagonPatternLinearGradient() {7 return (8 <HexagonPattern9 radius={40}10 x={-1}11 y={-1}12 className={cn(13 "mask-[linear-gradient(to_bottom_right,white,transparent,transparent)]",14 )}15 />16 );17}Dashed Stroke
1"use client"2
3import { HexagonPattern } from "@/components/backgrounds/pxl/hexagon-pattern"4
5export default function HexagonPatternDashed() {6 return (7 <HexagonPattern radius={40} x={-1} y={-1} strokeDasharray="4 2" />8 )9}Spacing
1"use client"2
3import { HexagonPattern } from "@/components/backgrounds/pxl/hexagon-pattern"4
5export default function HexagonPatternSpacing() {6 return (7 <HexagonPattern gap={20} radius={40} x={-1} y={-1} />8 )9}Variants
Use the variant prop to change the colors of the grid.
1"use client";2
3import { HexagonPattern } from "@/components/backgrounds/pxl/hexagon-pattern";4import { cn } from "@/lib/utils";5
6export default function HexagonPatternVariants() {7 return (8 <div className="flex flex-wrap size-full items-center justify-center gap-2">9 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">10 <HexagonPattern11 variant="muted"12 hexagons={[13 [1, 1],14 [4, 4],15 [2, 2],16 [3, 4],17 [5, 4],18 [8, 2],19 [6, 3],20 [8, 5],21 [10, 10],22 ]}23 className={cn(24 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",25 "inset-0 skew-y-6",26 )}27 />28 </div>29 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">30 <HexagonPattern31 variant="primary"32 hexagons={[33 [1, 1],34 [4, 4],35 [2, 2],36 [3, 4],37 [5, 4],38 [8, 2],39 [6, 3],40 [8, 5],41 [10, 10],42 ]}43 className={cn(44 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",45 "inset-0 skew-y-6",46 )}47 />48 </div>49 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">50 <HexagonPattern51 variant="info"52 hexagons={[53 [1, 1],54 [4, 4],55 [2, 2],56 [3, 4],57 [5, 4],58 [8, 2],59 [6, 3],60 [8, 5],61 [10, 10],62 ]}63 className={cn(64 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",65 "inset-0 skew-y-6",66 )}67 />68 </div>69 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">70 <HexagonPattern71 variant="success"72 hexagons={[73 [1, 1],74 [4, 4],75 [2, 2],76 [3, 4],77 [5, 4],78 [8, 2],79 [6, 3],80 [8, 5],81 [10, 10],82 ]}83 className={cn(84 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",85 "inset-0 skew-y-6",86 )}87 />88 </div>89 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">90 <HexagonPattern91 variant="warning"92 hexagons={[93 [1, 1],94 [4, 4],95 [2, 2],96 [3, 4],97 [5, 4],98 [8, 2],99 [6, 3],100 [8, 5],101 [10, 10],102 ]}103 className={cn(104 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",105 "inset-0 skew-y-6",106 )}107 />108 </div>109 <div className="relative w-max h-32 aspect-video pixel-rounded pixel-size-lg">110 <HexagonPattern111 variant="danger"112 hexagons={[113 [1, 1],114 [4, 4],115 [2, 2],116 [3, 4],117 [5, 4],118 [8, 2],119 [6, 3],120 [8, 5],121 [10, 10],122 ]}123 className={cn(124 "mask-[radial-gradient(420px_circle_at_center,white,transparent)]",125 "inset-0 skew-y-6",126 )}127 />128 </div>129 </div>130 );131}API Reference
HexagonPattern
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "primary" | "secondary" | "muted" | "success" | "warning" | "danger" |
"default" |
Colors of the grids |
radius |
number |
40 |
Radius from center to vertex for each hexagon |
gap |
number |
0 |
Extra spacing between adjacent hexagons (pixels) |
x |
number |
-1 |
X offset of the pattern origin |
y |
number |
-1 |
Y offset of the pattern origin |
direction |
"horizontal" | "vertical" |
"horizontal" |
horizontal — flat-top honeycomb; vertical — pointy-top honeycomb |
hexagons |
Array<[col, row]> |
— | Grid coordinates of highlighted (filled) hexagons |
strokeDasharray |
string |
"0" |
SVG stroke-dasharray for outline hexagons in the repeating pattern |
className |
string |
— | Additional classes on the root SVG (for example Tailwind color utilities) |