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 the default slot.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/watermark.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/watermark.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/watermark.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/watermark.json Named registry:
npx shadcn-vue@latest add @uipkge/watermark Installs to: app/components/ui/watermark/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
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 | -22 | optional |
gap Gap between repeats in pixels (both x and y). | number | 100 | optional |
opacity Opacity 0-1. | number | 0.08 | optional |
fontSize Font size in pixels (text only). | number | 16 | optional |
color Font color (text only). | string | 'currentColor' | optional |
fontFamily Font family (text only). | string | 'sans-serif' | optional |
fontWeight Font weight (text only). | number | string | 'normal' | optional |
zIndex z-index of the overlay. | number | 9 | optional |
interactive When true, the overlay captures pointer events (blocks interaction). Default false (pointer-events-none). | boolean | false | optional |
Files installed (2)
-
app/components/ui/watermark/Watermark.vue 4.1 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { cn } from '@/lib/utils' const props = withDefaults( defineProps<{ class?: HTMLAttributes['class'] /** 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 }>(), { content: '', rotate: -22, gap: 100, opacity: 0.08, fontSize: 16, color: 'currentColor', fontFamily: 'sans-serif', fontWeight: 'normal', zIndex: 9, interactive: false, }, ) const containerRef = ref<HTMLElement | null>(null) const width = ref(0) const height = ref(0) let resizeObserver: ResizeObserver | null = null function measure() { if (!containerRef.value) return const rect = containerRef.value.getBoundingClientRect() width.value = rect.width height.value = rect.height } onMounted(() => { measure() if (typeof ResizeObserver !== 'undefined' && containerRef.value) { resizeObserver = new ResizeObserver(() => measure()) resizeObserver.observe(containerRef.value) } }) onBeforeUnmount(() => { resizeObserver?.disconnect() resizeObserver = null }) // 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 = computed(() => { if (!width.value || !height.value) return '' const gap = props.gap const fontSize = props.fontSize const text = props.content || '' const rotate = props.rotate if (props.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="${props.image}" x="${gap / 2}" y="${gap / 2}" width="100" height="100" opacity="${props.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="${props.fontFamily}" font-weight="${props.fontWeight}" fill="${props.color}" opacity="${props.opacity}" transform="rotate(${rotate} ${gap / 2} ${gap / 2 + fontSize / 2})">${escapedText}</text> </svg>` return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}` }) const overlayStyle = computed(() => ({ backgroundImage: watermarkUrl.value ? `url("${watermarkUrl.value}")` : undefined, backgroundRepeat: 'repeat', zIndex: props.zIndex, pointerEvents: props.interactive ? 'auto' : 'none', })) </script> <template> <div ref="containerRef" data-uipkge data-slot="watermark" :class="cn('relative', props.class)"> <slot /> <div data-uipkge data-slot="watermark-overlay" class="absolute inset-0 overflow-hidden" :style="overlayStyle" aria-hidden="true" /> </div> </template> -
app/components/ui/watermark/index.ts 0.1 kB
export { default as Watermark } from './Watermark.vue'
Raw manifest: https://uipkge.dev/r/vue/watermark.json