Files
triqura-ecd/docs/design/marketing-shader-implementation.md
2025-11-15 22:06:48 +01:00

10 KiB

🚀 Marketing Shader Implementation Guide

Doel: Praktische implementatie van dot-shader component voor marketing website hero section.


📐 Design Specificaties

Hero Section Layout

┌─────────────────────────────────────┐
│  [Shader Background - zeer subtiel] │
│                                     │
│  ┌─────────────────────────────┐   │
│  │  Hero Content Overlay       │   │
│  │  - Headline (wit/donker)    │   │
│  │  - Subheadline              │   │
│  │  - CTA Buttons              │   │
│  │  - Live Metrics Counter     │   │
│  └─────────────────────────────┘   │
│                                     │
└─────────────────────────────────────┘

Visuele Hiërarchie

  1. Shader: Opacity 0.02-0.03 (bijna onzichtbaar, maar aanwezig)
  2. Content Overlay: Semi-transparant of solid (afhankelijk van contrast)
  3. Tekst: Hoog contrast (wit op donker, of donker op licht)

🔧 Technische Implementatie

Stap 1: Aangepaste Marketing Variant

// components/ui/marketing-shader-background.tsx
'use client'

import { DotScreenShader } from './dot-shader-background'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'

interface MarketingShaderProps {
  variant?: 'hero' | 'section'
  className?: string
}

export function MarketingShader({ variant = 'hero', className }: MarketingShaderProps) {
  const { theme } = useTheme()
  const [mounted, setMounted] = useState(false)
  const [reducedMotion, setReducedMotion] = useState(false)

  useEffect(() => {
    setMounted(true)
    // Check for reduced motion preference
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
    setReducedMotion(mediaQuery.matches)
    
    const handleChange = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
    mediaQuery.addEventListener('change', handleChange)
    return () => mediaQuery.removeEventListener('change', handleChange)
  }, [])

  // Fallback voor mobile of reduced motion
  if (!mounted || reducedMotion) {
    return (
      <div 
        className={`absolute inset-0 bg-gradient-to-br from-slate-50 via-blue-50/30 to-slate-100 ${className}`}
        aria-hidden="true"
      />
    )
  }

  // Desktop: shader component
  return (
    <div className={`absolute inset-0 overflow-hidden ${className}`} aria-hidden="true">
      <DotScreenShader />
      {/* Subtle overlay voor betere tekst leesbaarheid */}
      <div className="absolute inset-0 bg-white/40 dark:bg-slate-900/40 pointer-events-none" />
    </div>
  )
}

Stap 2: Hero Section Component

// app/(marketing)/components/hero-section.tsx
import { MarketingShader } from '@/components/ui/marketing-shader-background'
import { LiveMetricsCounter } from './live-metrics-counter'

export function HeroSection() {
  return (
    <section className="relative min-h-screen flex items-center justify-center overflow-hidden">
      {/* Shader Background */}
      <MarketingShader variant="hero" className="z-0" />
      
      {/* Content Overlay */}
      <div className="relative z-10 container mx-auto px-4 py-20">
        <div className="max-w-4xl mx-auto text-center">
          {/* Headline */}
          <h1 className="text-5xl md:text-7xl font-bold text-slate-900 dark:text-white mb-6">
            Software on Demand
          </h1>
          
          {/* Subheadline */}
          <p className="text-xl md:text-2xl text-slate-700 dark:text-slate-300 mb-8">
            Van 100.000 en 12 maanden naar 200 en 4 weken
          </p>
          
          {/* Live Metrics */}
          <LiveMetricsCounter />
          
          {/* CTAs */}
          <div className="flex flex-col sm:flex-row gap-4 justify-center mt-12">
            <button className="px-8 py-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
              Bekijk Demo
            </button>
            <button className="px-8 py-4 bg-slate-200 text-slate-900 rounded-lg hover:bg-slate-300 transition">
              Lees Manifesto
            </button>
          </div>
        </div>
      </div>
    </section>
  )
}

Stap 3: Shader Component Aanpassingen

// Aanpassingen in dot-shader-background.tsx voor marketing gebruik

// In Scene component, update getThemeColors:
const getThemeColors = () => {
  switch (theme) {
    case 'light':
      return {
        dotColor: '#E2E8F0',      // Match UX stylesheet border color
        bgColor: '#F8FAFC',        // Match UX stylesheet app background
        dotOpacity: 0.03           // Zeer subtiel voor marketing
      }
    case 'dark':
      return {
        dotColor: '#475569',       // Match secondary text
        bgColor: '#0F172A',        // Match primary text (inverted)
        dotOpacity: 0.02           // Nog subtieler
      }
    default:
      return {
        dotColor: '#E2E8F0',
        bgColor: '#F8FAFC',
        dotOpacity: 0.03
      }
  }
}

// Update gridSize voor eleganter effect
const gridSize = 80  // Fijner dan standaard 100

🎨 Styling Integratie

Tailwind Classes voor Content Overlay

/* Zorg voor goede contrast over shader */
.hero-content {
  /* Optioneel: semi-transparante achtergrond voor tekst */
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(10px);
  
  /* Of: solid achtergrond met padding */
  background: white;
  padding: 2rem;
  border-radius: 1rem;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

Contrast Check

Test Scenario's:

  1. Wit tekst op shader achtergrond → Minimaal 4.5:1 contrast
  2. Donker tekst op shader achtergrond → Minimaal 4.5:1 contrast
  3. Met overlay → Contrast moet verbeteren

Tools:


📱 Mobile Responsive

Breakpoint Strategie

// components/ui/marketing-shader-background.tsx

export function MarketingShader({ variant, className }: MarketingShaderProps) {
  const [isMobile, setIsMobile] = useState(false)

  useEffect(() => {
    const checkMobile = () => {
      setIsMobile(window.innerWidth < 768) // Tailwind md breakpoint
    }
    checkMobile()
    window.addEventListener('resize', checkMobile)
    return () => window.removeEventListener('resize', checkMobile)
  }, [])

  // Mobile: gradient fallback (performance)
  if (isMobile) {
    return (
      <div 
        className={`absolute inset-0 bg-gradient-to-br from-slate-50 via-blue-50/20 to-slate-100 ${className}`}
        aria-hidden="true"
      />
    )
  }

  // Desktop: shader
  return <DotScreenShader />
}

Reden:

  • Mobile GPU performance beperkt
  • Batterij impact
  • UX: Gebruikers verwachten snelle laadtijden op mobile

Performance Optimalisatie

Lazy Loading

import dynamic from 'next/dynamic'

// Lazy load shader alleen wanneer hero in viewport
const MarketingShader = dynamic(
  () => import('@/components/ui/marketing-shader-background').then(mod => mod.MarketingShader),
  { 
    ssr: false, // Client-side only
    loading: () => <div className="absolute inset-0 bg-slate-50" />
  }
)

Intersection Observer

// components/ui/marketing-shader-background.tsx
export function MarketingShader({ variant, className }: MarketingShaderProps) {
  const [shouldRender, setShouldRender] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!ref.current) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setShouldRender(true)
          observer.disconnect()
        }
      },
      { threshold: 0.1 }
    )

    observer.observe(ref.current)
    return () => observer.disconnect()
  }, [])

  if (!shouldRender) {
    return <div ref={ref} className={`absolute inset-0 bg-slate-50 ${className}`} />
  }

  return <DotScreenShader />
}

Toegankelijkheid

Reduced Motion Support

// Automatisch detecteren en respecteren
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches

if (prefersReducedMotion) {
  // Geen animaties, statische gradient
  return <StaticGradientBackground />
}

Skip Animation Knop

// Optioneel: knop om animatie uit te zetten
<button 
  onClick={() => setAnimationEnabled(false)}
  className="sr-only focus:not-sr-only"
>
  Skip animatie
</button>

ARIA Labels

<div 
  className="absolute inset-0"
  aria-hidden="true"  // Decoratief element, niet voor screen readers
  role="presentation"
>
  <DotScreenShader />
</div>

📊 Monitoring & Metrics

Performance Tracking

// Track shader performance
useEffect(() => {
  const startTime = performance.now()
  
  // Na render
  requestAnimationFrame(() => {
    const renderTime = performance.now() - startTime
    if (renderTime > 100) {
      console.warn('Shader render tijd hoog:', renderTime)
      // Mogelijk fallback activeren
    }
  })
}, [])

A/B Test Setup

// Variant A: Met shader
// Variant B: Zonder shader (gradient)

const useShader = () => {
  // Cookie/localStorage based variant assignment
  const variant = localStorage.getItem('hero-variant') || 'A'
  return variant === 'A'
}

Metrics te meten:

  • Time to Interactive (TTI)
  • First Contentful Paint (FCP)
  • Bounce rate
  • Engagement tijd
  • Conversion rate

Checklist

Pre-Launch

  • Shader opacity op 0.02-0.03 (zeer subtiel)
  • Kleuren matchen exact UX stylesheet
  • Mobile fallback geïmplementeerd
  • Reduced motion support
  • Contrast check gedaan (WCAG AA)
  • Performance test (< 100ms render tijd)
  • Lazy loading geïmplementeerd

Post-Launch

  • Performance monitoring actief
  • A/B test resultaten analyseren
  • Gebruikersfeedback verzamelen
  • Accessibility audit uitgevoerd

🎯 Conclusie

Het shader component kan strategisch gebruikt worden op de marketing website, mits:

  1. Zeer lage opacity (0.02-0.03)
  2. Alleen in hero section
  3. Mobile fallback aanwezig
  4. Toegankelijkheid gewaarborgd
  5. Performance geoptimaliseerd

Resultaat: Innovatieve uitstraling zonder UX compromissen.