Watermark
watermark ui Overlay watermark that repeats text or an image across the content area at a configurable angle. Supports rotate angle, gap between repeats, opacity, font size/color/weight, z-index, and pointer-event control. Wrap any content via children.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/watermark.json $ npx shadcn@latest add https://uipkge.dev/r/react/watermark.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/watermark.json $ bunx shadcn@latest add https://uipkge.dev/r/react/watermark.json Named registry:
npx shadcn@latest add @uipkge-react/watermark Installs to: components/ui/watermark/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
content Text content for the watermark. Ignored when `image` is set. | string | — | optional |
image Image URL. When set, repeats the image instead of text. | string | — | optional |
rotate Rotation angle in degrees. | number | — | optional |
gap Gap between repeats in pixels (both x and y). | number | — | optional |
opacity Opacity 0-1. | number | — | optional |
fontSize Font size in pixels (text only). | number | — | optional |
color Font color (text only). | string | — | optional |
fontFamily Font family (text only). | string | — | optional |
fontWeight Font weight (text only). | number | string | — | optional |
zIndex z-index of the overlay. | number | — | optional |
interactive When true, the overlay captures pointer events (blocks interaction). Default false (pointer-events-none). | boolean | — | optional |
Files installed (2)
-
components/ui/watermark/Watermark.tsx 4.5 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface WatermarkProps extends React.HTMLAttributes<HTMLDivElement> { /** Text content for the watermark. Ignored when `image` is set. */ content?: string /** Image URL. When set, repeats the image instead of text. */ image?: string /** Rotation angle in degrees. */ rotate?: number /** Gap between repeats in pixels (both x and y). */ gap?: number /** Opacity 0-1. */ opacity?: number /** Font size in pixels (text only). */ fontSize?: number /** Font color (text only). */ color?: string /** Font family (text only). */ fontFamily?: string /** Font weight (text only). */ fontWeight?: number | string /** z-index of the overlay. */ zIndex?: number /** When true, the overlay captures pointer events (blocks interaction). Default false (pointer-events-none). */ interactive?: boolean } const Watermark = React.forwardRef<HTMLDivElement, WatermarkProps>( ( { className, children, content = '', image, rotate = -22, gap = 100, opacity = 0.08, fontSize = 16, color = 'currentColor', fontFamily = 'sans-serif', fontWeight = 'normal', zIndex = 9, interactive = false, ...props }, ref, ) => { const containerRef = React.useRef<HTMLDivElement | null>(null) const [size, setSize] = React.useState({ width: 0, height: 0 }) React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement) React.useEffect(() => { const el = containerRef.current if (!el) return const measure = () => { const rect = el.getBoundingClientRect() setSize({ width: rect.width, height: rect.height }) } measure() let ro: ResizeObserver | null = null if (typeof ResizeObserver !== 'undefined') { ro = new ResizeObserver(() => measure()) ro.observe(el) } return () => { ro?.disconnect() } }, []) // Build a tiled SVG data URL that repeats the text/image across a tile of // size (gap + contentSize). The SVG is then used as a background-image on // the overlay div, rotated to the requested angle. const watermarkUrl = React.useMemo(() => { if (!size.width || !size.height) return '' const text = content || '' if (image) { // For images we tile the image at its natural size within the gap. const tile = gap + 100 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${tile}" height="${tile}" viewBox="0 0 ${tile} ${tile}"> <image href="${image}" x="${gap / 2}" y="${gap / 2}" width="100" height="100" opacity="${opacity}" transform="rotate(${rotate} ${tile / 2} ${tile / 2})"/> </svg>` return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}` } if (!text) return '' // Estimate text width — rough heuristic, good enough for tiling. const textWidth = text.length * fontSize * 0.6 const tileW = gap + textWidth const tileH = gap + fontSize * 1.5 const escapedText = text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${tileW}" height="${tileH}" viewBox="0 0 ${tileW} ${tileH}"> <text x="${gap / 2}" y="${gap / 2 + fontSize}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${color}" opacity="${opacity}" transform="rotate(${rotate} ${gap / 2} ${gap / 2 + fontSize / 2})">${escapedText}</text> </svg>` return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}` }, [size.width, size.height, image, content, gap, fontSize, rotate, opacity, color, fontFamily, fontWeight]) const overlayStyle: React.CSSProperties = { backgroundImage: watermarkUrl ? `url("${watermarkUrl}")` : undefined, backgroundRepeat: 'repeat', zIndex, pointerEvents: interactive ? 'auto' : 'none', } return ( <div ref={containerRef} data-uipkge="" data-slot="watermark" className={cn('relative', className)} {...props}> {children} <div data-uipkge="" data-slot="watermark-overlay" className="absolute inset-0 overflow-hidden" style={overlayStyle} aria-hidden="true" /> </div> ) }, ) Watermark.displayName = 'Watermark' export { Watermark } -
components/ui/watermark/index.ts 0.1 kB
export { Watermark, type WatermarkProps } from './Watermark'
Raw manifest: https://uipkge.dev/r/react/watermark.json