UIPackage
Menu

Watermark

watermark ui
Edit on GitHub

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

$ npx 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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;')
    
          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