diff --git a/docs/USAGE.md b/docs/USAGE.md
index dce47df..a0327a7 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -23,6 +23,18 @@ The parent block acts as the controller and wrapper. It handles configuration, s
| `axis` | string | `'x'` | Carousel axis direction (`'x'` for horizontal, `'y'` for vertical). |
| `direction` | string | `'ltr'` | Carousel item direction: `'ltr'` (left-to-right) or `'rtl'` (right-to-left). |
| `slidesToScroll` | number | `1` | Number of slides to scroll per navigation action. |
+| `lazyLoadImages` | boolean | `true` | Add `loading="lazy"` to images in slides. Slides can opt out via `disableLazyLoadImages`. |
+
+---
+
+### Child Block: `carousel-kit/carousel-slide`
+The child block that serves as a container for individual slide content. Each slide can display custom blocks as defined by the parent's `allowedSlideBlocks` configuration.
+
+#### Attributes
+
+| Attribute | Type | Default | Description |
+| :-------------------------- | :------ | :------------ | :------------------------------------------ |
+| `disableLazyLoadImages` | boolean | `false` | Disable lazy loading for images in this slide (when carousel lazy loading is enabled). |
---
diff --git a/inc/Plugin.php b/inc/Plugin.php
index a296e70..5a3880a 100644
--- a/inc/Plugin.php
+++ b/inc/Plugin.php
@@ -10,6 +10,8 @@
namespace Rt_Carousel;
use Rt_Carousel\Traits\Singleton;
+use WP_Block;
+use WP_HTML_Tag_Processor;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
@@ -41,6 +43,7 @@ protected function setup_hooks(): void {
add_action( 'init', [ $this, 'register_block_patterns' ] );
add_action( 'admin_notices', [ $this, 'legacy_plugin_notice' ] );
add_action( 'network_admin_notices', [ $this, 'legacy_plugin_notice' ] );
+ add_filter( 'render_block_rt-carousel/carousel', [ $this, 'handle_lazy_load_images' ], 16, 3 );
}
/**
@@ -260,4 +263,57 @@ private function load_patterns_from_disk(): array {
return $data;
}
+
+ /**
+ * Add loading="lazy" to images in carousel slides.
+ *
+ * @param string $block_content The block content.
+ * @param array $parsed_block The parsed block.
+ * @param \WP_Block $instance The block instance.
+ *
+ * @return string Modified block content.
+ */
+ public function handle_lazy_load_images( string $block_content, array $parsed_block, WP_Block $instance ): string {
+ // $instance was added in WP 5.9.0, if it's not available, return the block content unmodified.
+ if ( ! $instance ) {
+ return $block_content;
+ }
+
+ // Bail early if the lazyLoadImages setting is not set.
+ if ( ! isset( $instance->attributes['lazyLoadImages'] ) ) {
+ return $block_content;
+ }
+
+ $lazy_load = (bool) $instance->attributes['lazyLoadImages'];
+
+ // If lazy loading is disabled, return as-is.
+ if ( ! $lazy_load ) {
+ return $block_content;
+ }
+
+ // Use WP_HTML_Tag_Processor to add loading="lazy" to
tags.
+ $processor = new WP_HTML_Tag_Processor( $block_content );
+ $slide_index = 0;
+
+ while ( $processor->next_tag( ) ) {
+ $tag = $processor->get_tag();
+
+ // Keep a track of the slide index to determine if an image is in the first slide or subsequent slides.
+ if ( 'DIV' === $tag && $processor->has_class( 'embla__slide' ) ) {
+ $slide_index++;
+ }
+
+ // If it's the first slide, set loading="lazy". For subsequent slides, set loading="eager" and fetchpriority="high".
+ if ( 'IMG' === $tag && null === $processor->get_attribute( 'loading' ) ) {
+ if ( 1 === $slide_index ) {
+ $processor->set_attribute( 'loading', 'eager' );
+ $processor->set_attribute( 'fetchpriority', 'high' );
+ } else {
+ $processor->set_attribute( 'loading', 'lazy' );
+ }
+ }
+ }
+
+ return $processor->get_updated_html();
+ }
}
diff --git a/src/blocks/carousel/__tests__/types.test.ts b/src/blocks/carousel/__tests__/types.test.ts
index aed1c05..6a063d7 100644
--- a/src/blocks/carousel/__tests__/types.test.ts
+++ b/src/blocks/carousel/__tests__/types.test.ts
@@ -33,6 +33,7 @@ describe( 'CarouselAttributes Type', () => {
ariaLabel: 'Image carousel',
slideGap: 16,
slidesToScroll: '1',
+ lazyLoadImages: true,
};
expect( attributes ).toBeDefined();
@@ -58,6 +59,7 @@ describe( 'CarouselAttributes Type', () => {
ariaLabel: '',
slideGap: 0,
slidesToScroll: 'auto',
+ lazyLoadImages: false,
};
// Verify all keys exist
diff --git a/src/blocks/carousel/block.json b/src/blocks/carousel/block.json
index e0df7ca..3fd53c1 100644
--- a/src/blocks/carousel/block.json
+++ b/src/blocks/carousel/block.json
@@ -85,10 +85,14 @@
"slidesToScroll": {
"type": "string",
"default": "1"
+ },
+ "lazyLoadImages": {
+ "type": "boolean",
+ "default": true
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScriptModule": "file:./view.js"
-}
\ No newline at end of file
+}
diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx
index 9edf641..a85a010 100644
--- a/src/blocks/carousel/edit.tsx
+++ b/src/blocks/carousel/edit.tsx
@@ -50,6 +50,7 @@ export default function Edit( {
autoplayStopOnMouseEnter,
ariaLabel,
slidesToScroll = '1',
+ lazyLoadImages,
} = attributes;
const [ emblaApi, setEmblaApi ] = useState();
@@ -241,6 +242,15 @@ export default function Edit( {
onChange={ ( value ) => setAttributes( { dragFree: value } ) }
help={ __( 'Enables momentum scrolling.', 'rt-carousel' ) }
/>
+ setAttributes( { lazyLoadImages: value } ) }
+ help={ __(
+ 'Load images only when they enter the viewport.',
+ 'carousel-kit',
+ ) }
+ />
;
export type CarouselSlideAttributes = {
verticalAlignment?: BlockVerticalAlignmentToolbar.Value;
+ disableLazyLoadImages?: boolean;
};
export type CarouselControlsAttributes = Record;
export type CarouselDotsAttributes = Record;