• Skip to primary navigation
  • Skip to main content
  • Home
  • GoldMine Tips
  • Best Bets
  • Contact

GM Maps

GoldMine CRM, SQL, WordPress, PHP, Ecommerce and More ...

  • Home
  • GoldMine Tips
  • Best Bets
  • Contact

Blog

A small ACF gallery carousel shortcode for GeneratePress sites

April 7, 2026 By Scott Pringle

Title: A small ACF gallery carousel shortcode for GeneratePress sites

I wanted a lightweight, vanilla way to drop an ACF gallery onto a page as a carousel. GenerateBlocks Pro 2.5 has a great Carousel block for slides you build in the editor, but I needed something for galleries that live in post meta on a custom post type. Most slider plugins try to do everything, and I just wanted one thing done well. So here it is.

It’s WordPress + ACF + Swiper. That’s the whole stack.

The shortcode renders an ACF gallery field as a Swiper carousel. No block, no UI, just [acf_gallery_carousel] on the page or in a template. It has the knobs I actually needed (height, arrows, dots, autoplay, loop, slides per view) and nothing else.

Usage

[acf_gallery_carousel]
[acf_gallery_carousel field="product_images" height="500"]
[acf_gallery_carousel height="600" autoplay="true" dots="false"]
[acf_gallery_carousel class="hero-gallery"]

The height attribute sets an image height in pixels and lets each image keep its natural width. If you’d rather style height in CSS, pass a class and set --agc-height on it. Either works.

The code

<?php
/**
 * ACF Gallery Carousel Shortcode
 * ------------------------------------------------------------------
 * A lightweight, vanilla shortcode for rendering an ACF gallery field
 * as a carousel. Built to work in a GeneratePress/GenerateBlocks site
 * but has no dependency on either, it's just WordPress + ACF + Swiper,
 * so it drops into any block-based theme.
 *
 * Why this exists:
 * I wanted a simple way to render an ACF gallery field as a carousel
 * on a custom post type detail page. GenerateBlocks Pro 2.5 ships a
 * great Carousel block, but it's designed for slides you author in
 * the editor, not for slides that live in structured post meta and
 * need to render dynamically per post. Slider plugins exist, but most
 * try to be everything and end up being heavy and opinionated. I just
 * wanted image-in, carousel-out with a few knobs.
 *
 * I also tried piggybacking on GB's carousel JavaScript directly.
 * Don't. Their view script is bundled per-block and coupled to their
 * block markup, reaching into it is fragile and breaks on updates.
 * Enqueueing Swiper directly is the sturdy path, and it's what most
 * carousel blocks (including GB's, near as I can tell) use under the
 * hood anyway.
 *
 * Tradeoffs to know about:
 * - Swiper loads from jsDelivr CDN. Fast, cached, but if you need a
 *   fully self-hosted site, drop the files in your theme and swap the
 *   two URLs in agc_register_assets().
 * - Assets enqueue from inside the shortcode as a fallback, which can
 *   cause a tiny flash on first render. For pages where you know the
 *   shortcode runs, pre-enqueue on wp_enqueue_scripts (see note below).
 * - Intentionally unstyled beyond the height plumbing. You style the
 *   arrows, dots, and spacing to match your site.
 *
 * Usage:
 *   [acf_gallery_carousel]
 *   [acf_gallery_carousel field="product_images" height="500"]
 *   [acf_gallery_carousel height="600" autoplay="true" dots="false"]
 *   [acf_gallery_carousel class="hero-gallery"]  // style height via CSS
 *
 * Attributes:
 *   id              Post ID (default: current post)
 *   field           ACF field name (default: gallery)
 *   image_size      WP registered image size (default: large)
 *   height          Image height in px, sets --agc-height inline
 *   class           Extra wrapper class for CSS-only height control
 *   arrows          true|false (default: true)
 *   dots            true|false (default: true)
 *   autoplay        true|false (default: false)
 *   autoplay_delay  ms (default: 4000)
 *   loop            true|false (default: true)
 *   slides_per_view auto|number (default: auto, keeps natural aspect ratios)
 *   space_between   px between slides (default: 16)
 *
 * Take this, make it yours, share what you learn.
 */

add_shortcode( 'acf_gallery_carousel', 'agc_shortcode' );
function agc_shortcode( $atts ) {

    $atts = shortcode_atts( array(
        'id'              => get_the_ID(),
        'field'           => 'gallery',
        'image_size'      => 'large',
        'height'          => '',
        'class'           => '',
        'arrows'          => 'true',
        'dots'            => 'true',
        'autoplay'        => 'false',
        'autoplay_delay'  => '4000',
        'loop'            => 'true',
        'slides_per_view' => 'auto',
        'space_between'   => '16',
    ), $atts, 'acf_gallery_carousel' );

    if ( ! function_exists( 'get_field' ) ) {
        return '';
    }

    $gallery = get_field( $atts['field'], $atts['id'] );
    if ( empty( $gallery ) || ! is_array( $gallery ) ) {
        return '';
    }

    // Fallback enqueue, for pages where we haven't pre-enqueued.
    agc_register_assets();
    wp_enqueue_style( 'agc-swiper' );
    wp_enqueue_script( 'agc-swiper' );

    // ACF gallery can return arrays, IDs, or URLs depending on field config.
    // We handle arrays (default) and IDs. Add a URL branch if you need it.
    $slides_html = '';
    foreach ( $gallery as $item ) {
        if ( is_array( $item ) && isset( $item['ID'] ) ) {
            $img_id  = (int) $item['ID'];
            $img_alt = isset( $item['alt'] ) ? $item['alt'] : '';
        } elseif ( is_numeric( $item ) ) {
            $img_id  = (int) $item;
            $img_alt = get_post_meta( $img_id, '_wp_attachment_image_alt', true );
        } else {
            continue;
        }

        $img = wp_get_attachment_image( $img_id, $atts['image_size'], false, array(
            'class'   => 'acf-gallery-carousel__image',
            'alt'     => esc_attr( $img_alt ),
            'loading' => 'lazy',
        ) );

        if ( $img ) {
            $slides_html .= '<div class="swiper-slide acf-gallery-carousel__slide">' . $img . '</div>';
        }
    }

    if ( '' === $slides_html ) {
        return '';
    }

    $options = array(
        'slidesPerView' => ( 'auto' === $atts['slides_per_view'] ) ? 'auto' : (int) $atts['slides_per_view'],
        'spaceBetween'  => (int) $atts['space_between'],
        'loop'          => filter_var( $atts['loop'],     FILTER_VALIDATE_BOOLEAN ),
        'arrows'        => filter_var( $atts['arrows'],   FILTER_VALIDATE_BOOLEAN ),
        'dots'          => filter_var( $atts['dots'],     FILTER_VALIDATE_BOOLEAN ),
        'autoplay'      => filter_var( $atts['autoplay'], FILTER_VALIDATE_BOOLEAN ),
        'autoplayDelay' => (int) $atts['autoplay_delay'],
    );

    // Inline height attribute overrides the CSS class default if both are set.
    $style = '';
    if ( '' !== $atts['height'] ) {
        $h     = is_numeric( $atts['height'] ) ? $atts['height'] . 'px' : $atts['height'];
        $style = ' style="--agc-height:' . esc_attr( $h ) . ';"';
    }

    $extra_class = $atts['class'] ? ' ' . esc_attr( $atts['class'] ) : '';

    ob_start();
    ?>
    <div
        class="acf-gallery-carousel swiper<?php echo $extra_class; ?>"
        data-agc-options="<?php echo esc_attr( wp_json_encode( $options ) ); ?>"
        <?php echo $style; ?>
    >
        <div class="swiper-wrapper acf-gallery-carousel__wrapper">
            <?php echo $slides_html; ?>
        </div>

        <?php if ( $options['dots'] ) : ?>
            <div class="swiper-pagination acf-gallery-carousel__pagination"></div>
        <?php endif; ?>

        <?php if ( $options['arrows'] ) : ?>
            <button class="swiper-button-prev acf-gallery-carousel__prev" aria-label="Previous slide"></button>
            <button class="swiper-button-next acf-gallery-carousel__next" aria-label="Next slide"></button>
        <?php endif; ?>
    </div>
    <?php
    return ob_get_clean();
}

/**
 * OPTIONAL: Pre-enqueue assets on pages where you know the shortcode runs.
 * This puts Swiper's CSS in <head> and avoids a flash on first render.
 * Customize the condition to match where you use the shortcode, examples:
 *
 *   if ( is_singular( 'your_cpt' ) )            // specific post type
 *   if ( is_page( array( 'gallery', 'work' ) )  // specific pages
 *   if ( is_singular() )                        // all single posts/pages
 */
add_action( 'wp_enqueue_scripts', 'agc_maybe_preload_assets' );
function agc_maybe_preload_assets() {
    // Uncomment and customize this condition for your site:
    // if ( is_singular( 'your_cpt' ) ) {
    //     agc_register_assets();
    //     wp_enqueue_style( 'agc-swiper' );
    //     wp_enqueue_script( 'agc-swiper' );
    // }
}

/**
 * Register Swiper assets, inline CSS, and init script. Idempotent.
 */
function agc_register_assets() {
    static $registered = false;
    if ( $registered ) {
        return;
    }
    $registered = true;

    wp_register_style(
        'agc-swiper',
        'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css',
        array(),
        '11'
    );

    wp_register_script(
        'agc

Drop it into a child theme’s functions.php, WPCodeBox, or wherever you keep snippets. It’s self-contained. Swiper enqueues on the pages where the shortcode runs.

Styling it

The snippet ships intentionally unstyled beyond the height plumbing, so it doesn’t fight your site’s design. Two things I ended up adding that you’ll probably want too:

Put the pagination dots below the images instead of over them:

css

.acf-gallery-carousel .swiper-pagination {
    position: static;
    margin-top: 1rem;
    transform: none;
}

Give the arrows a background puck so they read as buttons:

css

.acf-gallery-carousel .swiper-button-next,
.acf-gallery-carousel .swiper-button-prev {
    width: 44px;
    height: 44px;
    background: rgba(0, 0, 0, 0.75);
    border-radius: 50%;
}
.acf-gallery-carousel .swiper-button-next::after,
.acf-gallery-carousel .swiper-button-prev::after {
    font-size: 18px;
    color: #fff;
    font-weight: 700;
}

Tweak the alpha value, the border radius, and the button size to taste.

Tradeoffs to know about

Swiper loads from a CDN. Fast and cached, but if you need a fully self-hosted site, drop the files in your theme and swap the two URLs.

It only handles ACF’s Image Array and Image ID return formats. If yours is set to URL, you’ll need to add a short branch in the slide loop. Not hard, just flagging it.

It uses a shortcode, not a block. I know blocks are where the ecosystem is going, and if someone wants to wrap this in a block, go for it. I just needed something I could drop into a template and move on.

Thanks

I’ve taken plenty from what others in this community have shared, this is me passing some of that on. Take it, make it yours, share what you learn.

Filed Under: Uncategorized

GoldMine Tips

July 18, 2017 By Scott Pringle

GoldMine Tips

Filed Under: Uncategorized

Copyright© 2026 || Contact