Appearance
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',
],
];Example 2: Image Gallery
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)
}
}Related Documentation
- Component System - Component structure
- JavaScript Architecture - JS patterns
- Common Patterns - Common patterns
- ACF Integration - ACF examples
Next Steps:
- Review Component System for structure
- Check Common Patterns for patterns
- See JavaScript Architecture for JS examples