VECTOR
pnpm dlx shadcn@latest add @uigrove/vector-wordmark

Vector Wordmark

A heavy wordmark that turns into a dotted vector outline with editor handles and coordinates around the cursor.

Installation
pnpm dlx shadcn@latest add @uigrove/vector-wordmark

Dependencies: none. Installs @uigrove/dot-canvas alongside.

First time? Add the registry to components.json
{
  "registries": {
    "@uigrove": "https://uigrove.me/r/{name}.json"
  }
}
Usage
import { VectorWordmark } from '@/components/uigrove/vector-wordmark'

<VectorWordmark
  text="VECTOR"
  className="h-64 w-full"
/>
Props
NameTypeDefaultDescription
textstring'VECTOR'Text
pathstring''Optional path data drawn instead of the text
fontSizenumber160Font size
fontWeightnumber800Font weight
fontFamilystring''Font family
fitWidthbooleantrueFit width
colorstring'#ffffff'Color
strokeWidthnumber1.2Stroke width
dashnumber3Dot gap
reachnumber220Reach
speednumber0.5Speed
dampingnumber0.6Damping
guidesbooleantrueGuides
labelsbooleantrueLabels
interaction'hover' | 'auto' | 'none''hover'Interaction
durationnumber900Intro duration
staggernumber120Intro stagger
introbooleantrueIntro
classNamestring—Classes for the root element. Use it for sizing and position.
Code
'use client'

import { useEffect, useRef, type CSSProperties } from 'react'
import { useDotCanvas } from '@/hooks/use-dot-canvas'
import { VectorWordmarkEngine, type VectorWordmarkOptions } from './vector-wordmark.engine'

export interface VectorWordmarkProps extends Partial<Omit<VectorWordmarkOptions, 'text'>> {
  text: string
  className?: string
  style?: CSSProperties
}

export const vectorWordmarkDefaults: VectorWordmarkOptions = {
  text: 'VECTOR',
  path: '',
  fontSize: 160,
  fontWeight: 800,
  fontFamily: '',
  fitWidth: true,
  color: '#ffffff',
  strokeWidth: 1.2,
  dash: 3,
  reach: 220,
  speed: 0.5,
  damping: 0.6,
  guides: true,
  labels: true,
  interaction: 'hover',
  duration: 900,
  stagger: 120,
  intro: true,
}

export function VectorWordmark({
  text,
  path = vectorWordmarkDefaults.path,
  fontSize = vectorWordmarkDefaults.fontSize,
  fontWeight = vectorWordmarkDefaults.fontWeight,
  fontFamily = vectorWordmarkDefaults.fontFamily,
  fitWidth = vectorWordmarkDefaults.fitWidth,
  color = vectorWordmarkDefaults.color,
  strokeWidth = vectorWordmarkDefaults.strokeWidth,
  dash = vectorWordmarkDefaults.dash,
  reach = vectorWordmarkDefaults.reach,
  speed = vectorWordmarkDefaults.speed,
  damping = vectorWordmarkDefaults.damping,
  guides = vectorWordmarkDefaults.guides,
  labels = vectorWordmarkDefaults.labels,
  interaction = vectorWordmarkDefaults.interaction,
  duration = vectorWordmarkDefaults.duration,
  stagger = vectorWordmarkDefaults.stagger,
  intro = vectorWordmarkDefaults.intro,
  className,
  style,
}: VectorWordmarkProps) {
  const engineRef = useRef<VectorWordmarkEngine | null>(null)
  if (!engineRef.current) engineRef.current = new VectorWordmarkEngine(vectorWordmarkDefaults)
  const engine = engineRef.current
  const { containerRef, canvasRef, wake } = useDotCanvas(engine, {
    pointer: interaction === 'hover',
  })

  useEffect(() => {
    const container = containerRef.current
    if (!container) return
    const family = fontFamily || getComputedStyle(container).fontFamily
    const options: VectorWordmarkOptions = {
      text,
      path,
      fontSize,
      fontWeight,
      fontFamily: family,
      fitWidth,
      color,
      strokeWidth,
      dash,
      reach,
      speed,
      damping,
      guides,
      labels,
      interaction,
      duration,
      stagger,
      intro,
    }
    engine.setOptions(options)
    wake()
    let cancelled = false
    document.fonts?.load(`${fontWeight} ${fontSize}px ${family}`).then(() => {
      if (cancelled) return
      engine.setOptions({ ...options })
      engine.resize(
        container.clientWidth,
        container.clientHeight,
        Math.min(window.devicePixelRatio || 1, 2),
      )
      wake()
    })
    return () => {
      cancelled = true
    }
  }, [
    engine,
    wake,
    containerRef,
    text,
    path,
    fontSize,
    fontWeight,
    fontFamily,
    fitWidth,
    color,
    strokeWidth,
    dash,
    reach,
    speed,
    damping,
    guides,
    labels,
    interaction,
    duration,
    stagger,
    intro,
  ])

  return (
    <div
      ref={containerRef}
      className={['uig-vector-wordmark', 'pointer-events-none overflow-hidden', className]
        .filter(Boolean)
        .join(' ')}
      style={style}
    >
      <canvas
        ref={canvasRef}
        className="uig-vector-wordmark__canvas block h-full w-full"
        aria-hidden="true"
        role="presentation"
      />
      <span className="uig-vector-wordmark__text sr-only">{text}</span>
    </div>
  )
}