Skip to content

Examples & Recipes

This guide provides real-world component examples and recipes from LamaPress projects. Use these as starting points for your own components.

Table of Contents

Component Examples

Example 1: Simple Content Block

A basic content block with title and content:

components/blocks/content_block/index.php:

php
<?php
$classes = $classes ?? '';
$title = $fields['title'] ?? false;
$content = $fields['content'] ?? false;
?>

<?php if ($title || $content): ?>
    <div class="ll-block--content-block <?= $classes ?>">
        <div class="ll-container py-8">
            <?php if ($title): ?>
                <h2 class="ll-heading-2xl mb-4"><?= esc_html($title) ?></h2>
            <?php endif; ?>
            
            <?php if ($content): ?>
                <div class="ll-body-lg">
                    <?= wp_kses_post($content) ?>
                </div>
            <?php endif; ?>
        </div>
    </div>
<?php endif; ?>

components/blocks/content_block/acf.php:

php
<?php
$groupFields = [
    [
        'key' => 'title',
        'label' => 'Title',
        'name' => 'title',
        'type' => 'text',
    ],
    [
        'key' => 'content',
        'label' => 'Content',
        'name' => 'content',
        'type' => 'wysiwyg',
        'toolbar' => 'full',
    ],
];

A responsive image gallery:

components/blocks/image_gallery/index.php:

php
<?php
$classes = $classes ?? '';
$images = $fields['images'] ?? [];
?>

<?php if ($images): ?>
    <div class="ll-block--image-gallery <?= $classes ?>" data-component="blocks/image_gallery">
        <div class="ll-container py-8">
            <div class="ll-grid">
                <?php foreach ($images as $image): ?>
                    <div class="col-span-12 md:col-span-6 lg:col-span-4">
                        <?php if ($image['url']): ?>
                            <img 
                                src="<?= esc_url($image['url']) ?>" 
                                alt="<?= esc_attr($image['alt'] ?? '') ?>"
                                class="w-full h-auto object-cover"
                                loading="lazy"
                            >
                        <?php endif; ?>
                    </div>
                <?php endforeach; ?>
            </div>
        </div>
    </div>
<?php endif; ?>

components/blocks/image_gallery/acf.php:

php
<?php
$groupFields = [
    [
        'key' => 'images',
        'label' => 'Images',
        'name' => 'images',
        'type' => 'gallery',
        'return_format' => 'array',
        'preview_size' => 'medium',
    ],
];

JavaScript Examples

Example 1: Interactive Card

Card with hover and click interactions:

components/blocks/interactive_card/index.js:

javascript
import { gsap } from 'gsap/all'
import { killTimeline } from '@src/js/core/utils/helpers'

export default class InteractiveCard {
  constructor(element) {
    this.element = element
    this.card = this.element.querySelector('.js-card')
    this.hoverTimeline = null
    this.init()
  }

  init = () => {
    this.bindEvents()
  }

  bindEvents = () => {
    this.element.addEventListener('mouseenter', this.handleMouseEnter)
    this.element.addEventListener('mouseleave', this.handleMouseLeave)
    this.element.addEventListener('click', this.handleClick)
  }

  handleMouseEnter = () => {
    killTimeline(this.hoverTimeline)
    this.hoverTimeline = gsap.timeline()
    this.hoverTimeline.to(this.card, {
      y: -10,
      scale: 1.02,
      duration: 0.3,
      ease: 'power2.out'
    })
  }

  handleMouseLeave = () => {
    killTimeline(this.hoverTimeline)
    this.hoverTimeline = gsap.timeline()
    this.hoverTimeline.to(this.card, {
      y: 0,
      scale: 1,
      duration: 0.3,
      ease: 'power2.out'
    })
  }

  handleClick = () => {
    // Handle click
    console.log('Card clicked')
  }

  destroy = () => {
    killTimeline(this.hoverTimeline)
    this.element.removeEventListener('mouseenter', this.handleMouseEnter)
    this.element.removeEventListener('mouseleave', this.handleMouseLeave)
    this.element.removeEventListener('click', this.handleClick)
  }
}

Example 2: Scroll-Triggered Counter

Animated counter that counts up on scroll:

components/blocks/counter/index.js:

javascript
import { gsap } from 'gsap/all'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { killTimeline } from '@src/js/core/utils/helpers'

export default class Counter {
  constructor(element) {
    this.element = element
    this.numberElement = this.element.querySelector('.js-number')
    this.targetValue = parseInt(this.numberElement.textContent) || 0
    this.timeline = null
    this.init()
  }

  init = () => {
    this.createAnimation()
  }

  createAnimation = () => {
    gsap.set(this.numberElement, { textContent: 0 })
    
    this.timeline = gsap.timeline({
      scrollTrigger: {
        trigger: this.element,
        start: 'top 80%',
        once: true,
      }
    })
    
    this.timeline.to({ value: 0 }, {
      value: this.targetValue,
      duration: 2,
      ease: 'power1.out',
      onUpdate: () => {
        this.numberElement.textContent = Math.round(this.timeline.progress() * this.targetValue)
      }
    })
  }

  destroy = () => {
    killTimeline(this.timeline)
    ScrollTrigger.getAll().forEach(trigger => {
      if (trigger.vars.trigger === this.element) {
        trigger.kill()
      }
    })
  }
}

ACF Examples

Example 1: Complex Repeater

Repeater with multiple sub-fields:

components/blocks/testimonials/acf.php:

php
<?php
$groupFields = [
    [
        'key' => 'testimonials',
        'label' => 'Testimonials',
        'name' => 'testimonials',
        'type' => 'repeater',
        'sub_fields' => [
            [
                'key' => 'quote',
                'label' => 'Quote',
                'name' => 'quote',
                'type' => 'textarea',
                'rows' => 4,
            ],
            [
                'key' => 'author',
                'label' => 'Author',
                'name' => 'author',
                'type' => 'text',
            ],
            [
                'key' => 'role',
                'label' => 'Role',
                'name' => 'role',
                'type' => 'text',
            ],
            [
                'key' => 'image',
                'label' => 'Image',
                'name' => 'image',
                'type' => 'image',
                'return_format' => 'array',
            ],
        ],
        'button_label' => 'Add Testimonial',
        'min' => 1,
    ],
];

Example 2: Conditional Fields

Fields that show/hide based on selections:

components/blocks/cta/acf.php:

php
<?php
$groupFields = [
    [
        'key' => 'style',
        'label' => 'Style',
        'name' => 'style',
        'type' => 'select',
        'choices' => [
            'default' => 'Default',
            'image' => 'With Image',
            'video' => 'With Video',
        ],
        'default_value' => 'default',
    ],
    [
        'key' => 'image',
        'label' => 'Image',
        'name' => 'image',
        'type' => 'image',
        'conditional_logic' => [
            [
                [
                    'field' => 'style',
                    'operator' => '==',
                    'value' => 'image',
                ],
            ],
        ],
    ],
    [
        'key' => 'video',
        'label' => 'Video',
        'name' => 'video',
        'type' => 'file',
        'mime_types' => 'mp4,webm',
        'conditional_logic' => [
            [
                [
                    'field' => 'style',
                    'operator' => '==',
                    'value' => 'video',
                ],
            ],
        ],
    ],
];

Complete Component Recipes

Recipe 1: Hero Section with Video Background

Complete hero section with video background and content overlay:

components/sections/hero_video/index.php:

php
<?php
$title = llField('title', $key);
$content = llField('content', $key);
$video = llField('video', $key);
$cta = llField('cta', $key);
?>

<section class="ll-section ll-section--hero-video" data-component="sections/hero_video">
    <?php if ($video): ?>
        <div class="ll-hero-video">
            <video autoplay muted loop playsinline>
                <source src="<?= esc_url($video['url']) ?>" type="video/mp4">
            </video>
        </div>
    <?php endif; ?>
    
    <div class="ll-hero-content">
        <div class="ll-container">
            <?php if ($title): ?>
                <h1 class="ll-heading-3xl mb-4"><?= esc_html($title) ?></h1>
            <?php endif; ?>
            
            <?php if ($content): ?>
                <div class="ll-body-xl mb-8"><?= wp_kses_post($content) ?></div>
            <?php endif; ?>
            
            <?php if ($cta): ?>
                <?php llPart('buttons/default', [
                    'text' => $cta['text'],
                    'url' => $cta['url'],
                ]); ?>
            <?php endif; ?>
        </div>
    </div>
</section>

components/sections/hero_video/index.js:

javascript
import { gsap } from 'gsap/all'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

export default class HeroVideo {
  constructor(element) {
    this.element = element
    this.video = this.element.querySelector('video')
    this.content = this.element.querySelector('.ll-hero-content')
    this.init()
  }

  init = () => {
    this.createRevealAnimation()
    this.bindEvents()
  }

  createRevealAnimation = () => {
    gsap.from(this.content, {
      opacity: 0,
      y: 30,
      duration: 1,
      ease: 'power3.out'
    })
  }

  bindEvents = () => {
    if (this.video) {
      this.video.addEventListener('loadedmetadata', () => {
        this.video.play()
      })
    }
  }

  destroy = () => {
    // Cleanup
  }
}

Recipe 2: Filterable Grid

Grid with filtering functionality:

components/blocks/filterable_grid/index.php:

php
<?php
$classes = $classes ?? '';
$items = $fields['items'] ?? [];
$categories = $fields['categories'] ?? [];
?>

<div class="ll-block--filterable-grid <?= $classes ?>" data-component="blocks/filterable_grid">
    <?php if ($categories): ?>
        <div class="ll-filter-buttons">
            <button class="js-filter-btn active" data-filter="all">All</button>
            <?php foreach ($categories as $category): ?>
                <button class="js-filter-btn" data-filter="<?= esc_attr($category['slug']) ?>">
                    <?= esc_html($category['name']) ?>
                </button>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>
    
    <div class="ll-grid js-grid">
        <?php foreach ($items as $item): ?>
            <div class="col-span-12 md:col-span-6 lg:col-span-4 js-grid-item" 
                 data-category="<?= esc_attr($item['category']) ?>">
                <!-- Item content -->
            </div>
        <?php endforeach; ?>
    </div>
</div>

components/blocks/filterable_grid/index.js:

javascript
import { gsap } from 'gsap/all'
import { killTimeline } from '@src/js/core/utils/helpers'

export default class FilterableGrid {
  constructor(element) {
    this.element = element
    this.buttons = [...this.element.querySelectorAll('.js-filter-btn')]
    this.items = [...this.element.querySelectorAll('.js-grid-item')]
    this.activeFilter = 'all'
    this.timeline = null
    this.init()
  }

  init = () => {
    this.bindEvents()
  }

  bindEvents = () => {
    this.buttons.forEach(button => {
      button.addEventListener('click', () => {
        const filter = button.dataset.filter
        this.filter(filter)
        this.updateActiveButton(button)
      })
    })
  }

  filter = (filter) => {
    this.activeFilter = filter
    killTimeline(this.timeline)
    
    this.items.forEach(item => {
      const category = item.dataset.category
      const shouldShow = filter === 'all' || category === filter
      
      if (shouldShow) {
        item.style.display = ''
        gsap.from(item, {
          opacity: 0,
          scale: 0.8,
          duration: 0.3
        })
      } else {
        gsap.to(item, {
          opacity: 0,
          scale: 0.8,
          duration: 0.3,
          onComplete: () => {
            item.style.display = 'none'
          }
        })
      }
    })
  }

  updateActiveButton = (activeButton) => {
    this.buttons.forEach(button => {
      button.classList.remove('active')
    })
    activeButton.classList.add('active')
  }

  destroy = () => {
    killTimeline(this.timeline)
  }
}

Next Steps:

Released under the MIT License.