ACF Post Grid — Dynamic Source with Grid or Carousel Layout

An ACF Flexible Content post-grid layout with two data sources — latest posts via WP_Query or hand-picked posts via a Relationship field — and two display modes: CSS grid or CSS scroll-snap carousel. Switch source and layout from the ACF panel, no code changes.

PHP component Production-tested ~10 min setup ACF Pro required Intermediate WP 6.0+ · ACF 6.0+ Updated May 17, 2026
TL;DR

An ACF Flexible Content post-grid layout with two data sources — latest posts via WP_Query or hand-picked posts via a Relationship field — and two display modes: CSS grid or CSS scroll-snap carousel. Switch source and layout from the ACF panel, no code changes.

Use this when you need a reusable "posts" section on any page — homepage featured articles, a related projects row, a blog highlights strip — where the editor controls what appears and how it's displayed without touching code.

01 — Building it step by step

Building It Step by Step

5 steps from zero to a working post grid. Create the ACF fields first, then build the PHP one branch at a time — the code grows by exactly one new piece each step.

Build a post grid from scratch in 5 steps. Start by creating the ACF fields so you know what names to use in PHP, then write one branch at a time.

Step 1: Create the ACF fields. Before writing any PHP, open ACF → Field Groups → Add New and create a field group called Post Grid. Add these 3 fields:

  1. Button Group — Label: Data Source, Name: data_source, Choices: latest : Latest and manual : Manual, Default: latest
  2. Group — Label: Latest Settings, Name: latest_settings. Conditional Logic: show when Data Source = latest. Inside this group, add two sub-fields:
    • Number — Label: Post Count, Name: post_count, Default: 3
    • Select — Label: Post Order, Name: post_order, Choices: DESC : Newest first and ASC : Oldest first, Default: DESC
  3. Relationship — Label: Manual Posts, Name: manual_posts, Return Format: Post Object. Conditional Logic: show when Data Source = manual

Assign the field group to Pages. Save.

acf-fields-reference.php
php
<?php
// The ACF fields you just created:
//
// data_source          — Button Group  — returns 'latest' or 'manual'
// latest_settings      — Group         — visible only when data_source = latest
//   └ post_count       — Number        — returns an integer (e.g. 3)
//   └ post_order       — Select        — returns 'DESC' or 'ASC'
// manual_posts         — Relationship  — visible only when data_source = manual
//                                        returns an array of WP_Post objects
//
// Conditional logic is on the group, not on each sub-field.
// The PHP reads the group in one call and gets both values as an array.

Step 2: Read the field value into a variable. Create template-parts/sections/post-grid.php and start with one line. This reads whatever the editor chose in the ACF panel:

step-2-read-field.php
php
<?php
$data_source = get_sub_field( 'data_source' );

// $data_source is now the string 'latest' or 'manual'

Step 3: Write the if/elseif skeleton. Before adding any real logic, write the structure first. This makes it obvious which source goes where — and it already works (it just outputs nothing yet):

step-3-skeleton.php
php
<?php
$data_source = get_sub_field( 'data_source' );

if ( $data_source === 'manual' ) {

    // manual content will go here

} elseif ( $data_source === 'latest' ) {

    // latest content will go here

}

Step 4: Fill in the manual branch. The Relationship field returns an array of WP_Post objects. Loop through them and echo each title. The latest branch still has its placeholder comment:

step-4-manual.php
php
<?php
$data_source = get_sub_field( 'data_source' );

if ( $data_source === 'manual' ) {

    $posts = get_sub_field( 'manual_posts' ) ?: [];

    foreach ( $posts as $post ) {
        echo '<p>' . esc_html( $post->post_title ) . '</p>';
    }

} elseif ( $data_source === 'latest' ) {

    // latest content will go here

}

Step 5: Fill in the latest branch. Get the latest_settings group in one call — it returns both values as an array. Then pass them into WP_Query:

step-5-latest.php
php
<?php
$data_source = get_sub_field( 'data_source' );

if ( $data_source === 'manual' ) {

    $posts = get_sub_field( 'manual_posts' ) ?: [];

    foreach ( $posts as $post ) {
        echo '<p>' . esc_html( $post->post_title ) . '</p>';
    }

} elseif ( $data_source === 'latest' ) {

    $latest     = get_sub_field( 'latest_settings' );
    $post_count = $latest['post_count'] ?? 3;
    $post_order = $latest['post_order'] ?? 'DESC';

    $query = new WP_Query( [
        'post_type'      => 'post',
        'posts_per_page' => $post_count,
        'orderby'        => 'date',
        'order'          => $post_order,
        'no_found_rows'  => true,
    ] );

    foreach ( $query->posts as $post ) {
        echo '<p>' . esc_html( $post->post_title ) . '</p>';
    }

}

That’s the core logic working. Both branches read ACF fields, fetch posts, and output titles. Section 02 explains why each part is written the way it is. Section 03 shows how to add ordering, a layout switcher, and styled post cards on top of this foundation.

02 — How it works

How It Works — Why Each Layer Exists

Five layers, each solving a specific problem from the previous step.

1

Button Group drives the branch — one field, two completely different data sources

The Button Group stores a string: latest or manual. An if/else checks that string and runs different code for each path. Both paths end with the same $posts array of post objects, so everything after the branch doesn’t care which source was used.

2

Order mapping — convert a plain string into WP_Query arguments

WP_Query needs orderby and order — it doesn’t understand “newest” or “alphabetical”. The $order_map array translates the editor’s choice into what WP_Query expects. array_merge() combines the base query args with the sort args. The ?? operator provides a safe fallback to “newest” if the key is missing.

3

Layout switching — one modifier class, zero JavaScript libraries

Grid and carousel use identical HTML. The only difference is one extra CSS class on the wrapper. When the editor picks Carousel, the PHP adds post-grid__grid--carousel. No carousel JS or CSS is included — that’s intentionally left to your theme so this works with any library.

4

render_post_card() — WP_Post in, safe HTML out

The function takes a WP_Post object — both WP_Query and the Relationship field return these by default. get_the_post_thumbnail() builds the complete img tag with srcset/sizes automatically. The image link uses tabindex="-1" and aria-hidden="true" because the title link below goes to the same URL — this prevents duplicate focus for keyboard/screen reader users.

5

Production safety — absint, early return, defensive defaults

absint() safely converts the Number field to a positive integer. An empty field (null) becomes 0, and 0 ?: 3 gives us the default of 3. The early return when $posts is empty prevents an empty wrapper div from appearing in a CSS grid/flexbox layout.

03 — Setup & ACF fields

Setup & ACF Fields

Two PHP files and one JS snippet. Everything wires together in about 10 minutes.

wp-content/themes/your-theme/
│   ├── template-parts/
│   │   └── sections/
│   │       └── post-grid.php
│   ├── inc/
│   │   └── post-card.php
    └── functions.php
1

Create the PHP files

Create the two PHP files:

  • template-parts/sections/post-grid.php — paste the Step 5 production code
  • inc/post-card.php — paste the Step 4 helper function

In functions.php, add:
require_once get_template_directory() . '/inc/post-card.php';

2

Register the layout in your Flexible Content field

If you are using a Flexible Content field, open it in ACF, find your FC field, and add a new layout named post-grid. Inside that layout, add the same sub-fields with the same names you created in Section 01. The sub-field names must match exactly what the PHP reads.

If you already created a standalone field group in Section 01, skip this step — your field group is already attached to the page.

3

Wire up your carousel library

Point your carousel library at .post-grid__grid--carousel. The PHP sets that class — your JavaScript handles the rest. Example with Swiper:

new Swiper('.post-grid__grid--carousel', { slidesPerView: 3, spaceBetween: 24 });
04 — Making it your own

Making It Your Own

Three common extensions and two things that break silently.

1

Add a section heading field

Add a section heading. Add a Text field called heading to the layout. In the template, before the grid wrapper:

$heading = get_sub_field('heading');
if ($heading) {
echo '<h2 class="post-grid__heading">' . esc_html($heading) . '</h2>';
}
2

Add category filtering to the latest source

Add category filtering. Add a Taxonomy field called filter_category (return format: Term IDs). In the WP_Query:

$terms = get_sub_field('filter_category');
if ($terms) {
$query_args['tax_query'] = [[
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $terms,
]];
}
3

Add a column count control

Add column count. Add a Select field called columns with choices 2, 3, 4. Pass it as a CSS custom property:

$columns = absint(get_sub_field('columns')) ?: 3;
echo '<div class="post-grid__grid" style="--columns:' . $columns . '">';

In CSS: .post-grid__grid { display: grid; grid-template-columns: repeat(var(--columns, 3), 1fr); }

Two things that commonly break:

1. Relationship field return format. render_post_card() expects WP_Post objects. If your Relationship field is set to return Post IDs, the function fails silently. Fix: set Return Format → Post Object in ACF.

2. Cards stretching in carousel mode. If .post-card has width: 100% or flex: 1 from your stylesheet, cards stretch instead of scrolling. Fix: add flex-shrink: 0 to .post-grid__grid--carousel .post-card.

0 comments

No comments yet. Be the first.

Leave a Reply

ACF Post Grid Flexible Content — What This Snippet Does

This ACF post grid flexible content snippet builds a dynamic posts section inside any ACF Flexible Content field.

The editor controls the ACF post grid flexible content layout entirely from the page panel — no PHP changes needed once the template is in place.

How the ACF Post Grid Flexible Content Layout Works

The ACF post grid flexible content section has two data sources. Latest fetches posts automatically via WP_Query. Manual lets the editor hand-pick posts using a Relationship field.

Both sources feed the same ACF post grid flexible content card template — a linked image, a linked title, and a Read More link.

The 6 Fields Inside the ACF Post Grid Flexible Content Layout

The ACF post grid flexible content layout uses six sub-fields: Button Group for source, Select for post type, Number for count, Select for order, Relationship for manual posts, and Button Group for layout.

ACF conditional logic hides the irrelevant fields automatically. When the editor switches the ACF post grid flexible content data source to Manual, the post type, count, and order fields disappear.

Grid and Carousel — ACF Post Grid Flexible Content Layout Options

The ACF post grid flexible content layout field switches the wrapper class between post-grid__grid and post-grid__grid--carousel.

Your theme’s JavaScript activates the carousel by targeting that class. The ACF post grid flexible content PHP does not ship any carousel CSS or JS — it works with Swiper, Slick, or any library you already use.

See the dynamic ACF post source snippet for a simpler version without the layout switcher, and the ACF flexible content renderer snippet for building the renderer system around it.

Importing the ACF Post Grid Flexible Content JSON

A complete importable ACF JSON file is included in Section 03. It contains all 6 sub-fields with instructions, defaults, and conditional logic pre-wired.

Drop it into your theme’s acf-json/ folder and import via Custom Fields → Tools → Import. The ACF post grid flexible content panel appears immediately on any page.

The Relationship field requires ACF Pro. All other ACF post grid flexible content fields work with the free version. See the ACF Relationship field reference and the WP_Query class reference for full documentation.

Compatible with WordPress 6.0 and ACF 6.0 and later. The ACF post grid flexible content template works alongside any other layouts in your Flexible Content field without conflict.

Who Is This ACF Post Grid For?

The ACF post grid template is ideal for WordPress developers building flexible page layouts for clients. It removes the need to hardcode post queries into templates.

Instead of writing a new WP_Query for every posts section, you build the ACF post grid once and the editor controls everything from the admin panel.

Combined with ACF Flexible Content, the ACF post grid becomes a reusable building block that works on any page of any WordPress site.

Chat on WhatsApp