Skip to content

Testimonials Component ​

Interactive testimonials section with draggable cards, smooth animations, and intersection observer effects. Showcases customer feedback in an engaging visual format.

Overview ​

The Testimonials component creates an interactive testimonials section where customer testimonial cards can be dragged around, creating a playful and engaging user experience. It includes smooth animations, responsive design, and intersection observer for scroll-triggered effects.

Preview ​

Testimonials Component Preview

Basic Usage ​

vue
<template>
  <section class="py-16">
    <Testimonials />
  </section>
</template>

<script setup lang="ts">
import Testimonials from '@/components/Testimonials.vue'
</script>

Features ​

🎯 Interactive Cards ​

  • Draggable testimonial cards
  • Touch support for mobile devices
  • Smooth drag animations
  • Cursor state changes (grab/grabbing)

✨ Scroll Animations ​

  • Intersection observer integration
  • Staggered card entrance animations
  • Fade-in and slide-up effects
  • Delay timing for each card

📱 Responsive Design ​

  • Mobile-optimized layout
  • Touch gesture support
  • Responsive card positioning
  • Adaptive text sizing

🌍 Internationalization ​

  • Full i18n support for all text content
  • Translatable testimonial content
  • Multi-language descriptions

Component Structure ​

vue
<template>
  <div class="testimonials-container">
    <!-- Section Header -->
    <h1 ref="testimonialsRef">{{ $t('testimonials.title') }}</h1>
    <h2>{{ $t('testimonials.bottomTitle') }}</h2>
    <p>{{ $t('testimonials.description') }}</p>
    
    <!-- Interactive Cards Area -->
    <div class="relative w-full lg:max-w-4xl max-w-2xl h-64">
      <div 
        v-for="(testimonial, index) in displayTestimonials"
        :key="index"
        class="absolute cursor-grab"
        :style="getCardTransform(index)"
        @mousedown="startDrag(index, $event)"
        @touchstart="startDrag(index, $event)"
      >
        <TestimonialCard 
          :imageUrl="testimonial.imageUrl"
          :firstName="testimonial.firstName"
          :lastName="testimonial.lastName"
          :title="testimonial.title"
          :content="testimonial.content"
        />
      </div>
    </div>
  </div>
</template>

Component API ​

Props ​

This component doesn't accept props. Testimonial data is managed internally.

Events ​

EventPayloadDescription
card-dragged{ cardIndex, position }Emitted when a card is dragged

Data Properties ​

PropertyTypeDescription
displayTestimonialsArrayArray of testimonial objects
isVisiblebooleanIntersection observer visibility state
isDraggingbooleanCurrent drag state

Drag Functionality ​

Drag Implementation ​

typescript
const startDrag = (cardIndex: number, event: MouseEvent | TouchEvent) => {
  isDragging.value = true
  currentDragIndex.value = cardIndex
  
  const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX
  const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY
  
  dragOffset.value = {
    x: clientX - cardPositions.value[cardIndex].x,
    y: clientY - cardPositions.value[cardIndex].y
  }
}

Touch Support ​

typescript
// Touch events for mobile
@touchstart="startDrag(index, $event)"
@touchmove="onDrag"
@touchend="stopDrag"

// Mouse events for desktop
@mousedown="startDrag(index, $event)"
@mousemove="onDrag"
@mouseup="stopDrag"

Animation Details ​

Intersection Observer ​

typescript
const { stop } = useIntersectionObserver(
  testimonialsRef,
  ([{ isIntersecting }]) => {
    isVisible.value = isIntersecting
  },
  { threshold: 0.3 }
)

Staggered Animations ​

vue
<div 
  class="transition-all duration-1000 ease-out"
  :class="[
    getTestimonialPosition(index),
    { 
      'delay-800': index === 0, 
      'delay-950': index === 1, 
      'delay-1100': index === 2, 
      'delay-1250': index === 3 
    }
  ]"
>

Card Transform ​

typescript
const getCardTransform = (index: number) => {
  return {
    x: cardPositions.value[index]?.x || 0,
    y: cardPositions.value[index]?.y || 0,
    rotation: cardPositions.value[index]?.rotation || 0
  }
}

Testimonial Data Structure ​

typescript
interface Testimonial {
  id: string
  imageUrl: string
  firstName: string
  lastName: string
  title: string
  content: string
  rating?: number
  zIndex?: number
}

Example Data ​

typescript
const testimonials = [
  {
    id: '1',
    imageUrl: '/testimonials/john-doe.jpg',
    firstName: 'John',
    lastName: 'Doe',
    title: 'CEO at TechCorp',
    content: 'ClawPlate helped us launch our SaaS in record time. Amazing!',
    rating: 5
  }
]

Internationalization ​

Configure testimonials content in your i18n files:

json
{
  "testimonials": {
    "title": "What Our Customers Say",
    "bottomTitle": "Trusted by 1000+",
    "bottomSubtitle": "developers worldwide",
    "bottomDescription": "Join the community of successful SaaS builders",
    "description": "See how ClawPlate has helped developers and entrepreneurs launch their SaaS products faster than ever before."
  }
}

Styling Details ​

Container Animation ​

vue
<div 
  class="flex flex-col items-center justify-center w-full transition-all duration-800 ease-out"
  :class="{ 
    'opacity-100 translate-y-0': isVisible, 
    'opacity-0 translate-y-24': !isVisible 
  }"
>

Card Positioning ​

vue
<div 
  class="absolute cursor-grab active:cursor-grabbing select-none transition-all duration-1000 ease-out"
  :style="{ 
    zIndex: testimonial.zIndex || 1,
    transform: `translate3d(${x}px, ${y}px, 0px) rotate(${rotation}deg) scale(1)`
  }"
>

Typography Hierarchy ​

vue
<!-- Main title -->
<h1 class="lg:text-5xl text-4xl font-bold font-heading text-center mb-4">

<!-- Secondary title -->
<h2 class="text-3xl font-bold text-gray-900 mb-2">

<!-- Description -->
<p class="text-lg text-gray-500 text-center mb-8 max-w-2xl">

Customization ​

Changing Card Layout ​

typescript
const getInitialPosition = (index: number) => {
  const positions = [
    { x: 0, y: 0, rotation: -5 },
    { x: 200, y: 20, rotation: 3 },
    { x: 100, y: 40, rotation: -2 },
    { x: 300, y: 10, rotation: 4 }
  ]
  return positions[index] || { x: 0, y: 0, rotation: 0 }
}

Custom Animation Timing ​

vue
<!-- Faster animations -->
<div class="transition-all duration-500 ease-out">

<!-- Custom delays -->
:class="{ 
  'delay-500': index === 0, 
  'delay-700': index === 1,
  'delay-900': index === 2 
}"

Different Card Arrangements ​

typescript
// Circular arrangement
const getCircularPosition = (index: number, total: number) => {
  const angle = (index / total) * 2 * Math.PI
  const radius = 150
  return {
    x: Math.cos(angle) * radius,
    y: Math.sin(angle) * radius,
    rotation: (angle * 180) / Math.PI
  }
}

TestimonialCard Integration ​

The component uses TestimonialCard for individual testimonials:

vue
<TestimonialCard 
  :imageUrl="testimonial.imageUrl"
  :firstName="testimonial.firstName"
  :lastName="testimonial.lastName"
  :title="testimonial.title"
  :content="testimonial.content"
  :rating="testimonial.rating"
/>

Accessibility ​

Keyboard Navigation ​

vue
<div 
  tabindex="0"
  @keydown="handleKeyboardDrag"
  role="button"
  :aria-label="`Testimonial from ${testimonial.firstName} ${testimonial.lastName}`"
>

Screen Reader Support ​

vue
<div aria-live="polite" class="sr-only">
  {{ isDragging ? 'Moving testimonial card' : 'Testimonial cards ready to interact' }}
</div>

Focus Management ​

typescript
const handleKeyboardDrag = (event: KeyboardEvent) => {
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
    event.preventDefault()
    // Handle keyboard-based card movement
  }
}

Performance Optimization ​

Efficient Drag Handling ​

typescript
// Use RAF for smooth animations
const updateCardPosition = () => {
  if (isDragging.value) {
    requestAnimationFrame(updateCardPosition)
  }
}

Memory Management ​

typescript
onUnmounted(() => {
  // Clean up event listeners
  document.removeEventListener('mousemove', onDrag)
  document.removeEventListener('mouseup', stopDrag)
  document.removeEventListener('touchmove', onDrag)
  document.removeEventListener('touchend', stopDrag)
})

Integration Examples ​

With Analytics ​

vue
<script setup lang="ts">
const trackTestimonialInteraction = (action: string, testimonialId: string) => {
  gtag('event', 'testimonial_interaction', {
    event_category: 'engagement',
    event_label: testimonialId,
    custom_parameter: action
  })
}

const startDrag = (cardIndex: number, event: Event) => {
  trackTestimonialInteraction('drag_start', displayTestimonials.value[cardIndex].id)
  // ... drag logic
}
</script>

With Custom Data Source ​

vue
<script setup lang="ts">
const { data: testimonials } = await useFetch('/api/testimonials')
const displayTestimonials = computed(() => testimonials.value || [])
</script>

Dependencies ​

  • TestimonialCard - Individual testimonial component
  • @vueuse/core - For intersection observer
  • Nuxt i18n - For internationalization
  • Vue 3 - Composition API and reactivity
  • Tailwind CSS - For styling and animations

Built with love by mhdevfr