Skip to content

Instantly share code, notes, and snippets.

@Jany-M
Created November 4, 2025 20:41
Show Gist options
  • Save Jany-M/f53a21ac7eefe7e8caec8fc978f9ecf3 to your computer and use it in GitHub Desktop.
Save Jany-M/f53a21ac7eefe7e8caec8fc978f9ecf3 to your computer and use it in GitHub Desktop.
ACF vs Gutenberg: Force field groups to appear BELOW content editor (instead of being stuck in sidebar)
<?php
/**
* Force ACF Field Groups to Show Below Gutenberg Editor
*
* This attempts to force the "normal" position of an ACF Field Group, to work in Gutenberg by using WordPress meta box hooks.
* This is a workaround and may not work reliably.
* The "normal" position is not officially supported by ACF in Gutenberg, as of November 2025.
*
* Developed by Jany Martelli @ Shambix - https://www.shambix.com
*
* --- How to use
* 1. Download this whole file, call it acf-force-below-editor.php or what works for you.
* 2. Upload it to your theme and include it in functions.php by adding a line of code.
* e.g. (if you placed the file in your the theme's root):
* include_once get_template_directory() . '/acf-force-below-editor.php';
*
* --- Why this works (for now)
* The spacer meta box creates a "normal" area below the editor so Gutenberg exposes a drop zone for meta boxes.
* ACF field groups with position "normal" are automatically placed below the editor for new users and new post types.
* If groups were previously auto-saved in the sidebar, the script auto-corrects and moves them below the editor.
* Per-user + per-post-type: WordPress stores the layout in user_meta and remembers your arrangement after you drag/drop.
* The script will auto-retrieve the ACF groups (and their set position) that have been assigned to the current post type, if any.
*
* --- FYIs
* The spacer is nearly invisible but always present if any "normal" ACF groups exist for the CPT (try to drag in, to see the area).
* You can drag ACF groups between sidebar and below-editor; your layout is saved per user and post type.
* If a group "disappears": Check Screen Options (top right) and make sure the field group is enabled for that user on that screen.
* If you remove the spacer AND no other "normal" meta boxes are present, the below-editor area may vanish and groups may jump back to sidebar.
* The default ordering only applies for users/post types that have never customized their layout; after you drag/drop, your arrangement persists.
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Method 1: Force meta boxes to appear below editor
* This tells WordPress to render ACF meta boxes in the "normal" context
*/
add_filter('use_block_editor_for_post', 'acf_custom_force_acf_below_editor', 10, 2);
function acf_custom_force_acf_below_editor($use_block_editor, $post) {
// We're NOT disabling Gutenberg - just trying to fix ACF position
// This alone won't fix it, but it's part of the solution
return $use_block_editor;
}
/**
* Method 2: Try to force ACF to use classic meta box rendering
* This might make ACF render below the editor
*/
add_filter('acf/settings/remove_wp_meta_box', '__return_false');
/**
* Method 3: Register meta boxes to force "below editor" placement
* This creates a placeholder that forces content down
*/
// Register late and receive current $post_type and $post so we can decide dynamically
add_action('add_meta_boxes', 'acf_custom_force_acf_placement', 999, 2);
function acf_custom_force_acf_placement($post_type, $post) {
// DEBUG: Enable to see what's happening
// error_log("=== acf_custom_force_acf_placement ===");
// error_log("Post Type: {$post_type}");
// error_log("Post ID: " . (is_object($post) ? $post->ID : 'none'));
// Decide per-screen if we actually need a spacer based on ACF groups
$should_add_spacer = false;
$has_applicable_groups = false;
if (function_exists('acf_get_field_groups')) {
// Use post_id to let ACF resolve ALL location rules (post type, template, taxonomy, etc.)
$post_id = is_object($post) ? (int) $post->ID : 0;
// Get matching groups for this edit screen
// 1) Prefer post_id (fully resolves location rules)
$groups = acf_get_field_groups(array('post_id' => $post_id));
// error_log("Groups with post_id: " . count($groups));
// 2) On post-new.php, post_id may be 0 and result empty; fall back to post_type
if ((empty($groups) || !is_array($groups)) && !empty($post_type)) {
$groups = acf_get_field_groups(array('post_type' => $post_type));
// error_log("Groups with post_type: " . count($groups));
}
// 3) As a last resort, fetch all and filter by position only (keeps behavior conservative)
if (empty($groups) || !is_array($groups)) {
$groups = acf_get_field_groups();
// error_log("All groups: " . count($groups));
}
if (!empty($groups) && is_array($groups)) {
foreach ($groups as $group) {
// DEBUG: Show each group
// error_log("Group: {$group['title']} | Key: {$group['key']} | Position: " . (isset($group['position']) ? $group['position'] : 'not set'));
// Skip explicit sidebar groups
$position = isset($group['position']) ? $group['position'] : 'normal';
if ($position !== 'side') {
$should_add_spacer = true;
$has_applicable_groups = true;
// error_log("Should add spacer: YES (found non-side group)");
break;
}
}
}
}
// error_log("Final decision - should_add_spacer: " . ($should_add_spacer ? 'YES' : 'NO'));
// Fallback: if ACF is missing, don't add spacer
if (!$should_add_spacer) {
return;
}
// Add a (visually empty) meta box that ensures a "normal" area exists under the editor
add_meta_box(
'acf_custom_acf_spacer',
__('Post Settings', 'default'),
'acf_custom_render_acf_spacer',
$post_type,
'normal',
'high'
);
// error_log("Spacer meta box added for post_type: {$post_type}");
}
function acf_custom_render_acf_spacer($post) {
// Output a minimal visible element so WordPress doesn't hide the empty meta box
// This ensures the drop zone always appears even when empty
echo '<div style="min-height:1px;opacity:0.01;" aria-hidden="true">&nbsp;</div>';
}
/**
* Method 4: Try to force ACF field groups to respect position
* This filters each field group to set the position explicitly
*/
add_filter('acf/load_field_group', 'acf_custom_force_field_group_position');
function acf_custom_force_field_group_position($field_group) {
// Only adjust positions in the admin post editor context
if (!is_admin()) {
return $field_group;
}
if (function_exists('get_current_screen')) {
$screen = get_current_screen();
if (!is_object($screen) || $screen->base !== 'post') {
return $field_group;
}
}
// Do not touch sidebar groups
if (isset($field_group['position']) && $field_group['position'] === 'side') {
return $field_group;
}
// For any group rendered on post edit screens, prefer the classic "normal" position
// This is harmless if already set and helps in cases where position is omitted.
$field_group['position'] = 'normal';
if (!isset($field_group['style']) || !$field_group['style']) {
$field_group['style'] = 'default';
}
return $field_group;
}
/**
* Provide a sensible DEFAULT meta box order for users who haven't customized it yet.
* This ensures ACF groups (non-side) appear in the new below-editor drop zone by default.
* Users can still drag & drop to personalize; WP will persist per-user afterwards.
*/
add_action('current_screen', 'acf_custom_register_default_metabox_order');
function acf_custom_register_default_metabox_order($screen) {
if (!is_object($screen)) {
return;
}
// Only on post edit screens (post.php / post-new.php)
if ($screen->base !== 'post') {
return;
}
$screen_id = $screen->id; // e.g., 'post', 'page', 'rubrica'
$post_type = $screen->post_type; // current CPT
$post_id = isset($_GET['post']) ? (int) $_GET['post'] : 0;
// Check for applicable ACF groups before registering filter
$has_applicable_groups = false;
if (function_exists('acf_get_field_groups')) {
$args = array('post_type' => $post_type);
if ($post_id) {
$args['post_id'] = $post_id;
}
$groups = acf_get_field_groups($args);
if (!empty($groups) && is_array($groups)) {
foreach ($groups as $group) {
$position = isset($group['position']) ? $group['position'] : 'normal';
if ($position !== 'side') {
$has_applicable_groups = true;
break;
}
}
}
}
if (!$has_applicable_groups) {
return;
}
// Register a dynamic filter for this specific screen
add_filter("get_user_option_meta-box-order_{$screen_id}", function($value) use ($post_id, $post_type, $screen_id) {
// DEBUG: Log what WordPress is passing us
// error_log("Screen: {$screen_id} | Value type: " . gettype($value) . " | Value: " . print_r($value, true));
// Get list of ACF groups that should be in normal area (position != 'side')
$acf_boxes_for_normal = array();
if (function_exists('acf_get_field_groups')) {
$args = array('post_type' => $post_type);
if ($post_id) {
$args['post_id'] = $post_id;
}
$groups = acf_get_field_groups($args);
if (!empty($groups) && is_array($groups)) {
foreach ($groups as $group) {
$position = isset($group['position']) ? $group['position'] : 'normal';
if ($position !== 'side' && !empty($group['key'])) {
// ACF meta box ids follow the pattern 'acf-group_{$key}'
// But $group['key'] might already include 'group_' prefix
$box_id = 'acf-' . $group['key'];
$acf_boxes_for_normal[] = $box_id;
// error_log("ACF box for normal: {$box_id} (from key: {$group['key']})");
}
}
}
}
// error_log("Total ACF boxes that should be in normal: " . count($acf_boxes_for_normal));
// If user has saved layout, check if ACF groups are misplaced in sidebar
if (is_array($value) && !empty($value)) {
// Check if any ACF groups that should be "normal" are stuck in "side"
$side_boxes = isset($value['side']) ? explode(',', $value['side']) : array();
$side_boxes = array_filter($side_boxes); // Remove empty strings
$normal_boxes = isset($value['normal']) ? explode(',', $value['normal']) : array();
$normal_boxes = array_filter($normal_boxes);
// error_log("Side boxes: " . print_r($side_boxes, true));
// error_log("ACF boxes for normal: " . print_r($acf_boxes_for_normal, true));
$misplaced_boxes = array();
foreach ($side_boxes as $box_id) {
// error_log("Checking if {$box_id} should be in normal...");
if (in_array($box_id, $acf_boxes_for_normal)) {
$misplaced_boxes[] = $box_id;
// error_log("YES - {$box_id} is misplaced!");
}
}
// If we found misplaced ACF groups, move them to normal and SAVE the correction
if (!empty($misplaced_boxes)) {
// error_log("Found misplaced ACF groups in sidebar: " . implode(', ', $misplaced_boxes));
// Remove from side
$side_boxes = array_diff($side_boxes, $misplaced_boxes);
// Add to normal (after spacer if it exists)
if (!in_array('acf_custom_acf_spacer', $normal_boxes)) {
array_unshift($normal_boxes, 'acf_custom_acf_spacer');
}
$normal_boxes = array_merge($normal_boxes, $misplaced_boxes);
// Rebuild the value
$corrected_value = array(
'normal' => implode(',', array_filter($normal_boxes)),
'side' => implode(',', array_filter($side_boxes)),
'advanced' => isset($value['advanced']) ? $value['advanced'] : '',
);
// SAVE the corrected layout to user meta so it persists
$user_id = get_current_user_id();
$meta_key = "meta-box-order_{$screen_id}";
update_user_meta($user_id, $meta_key, $corrected_value);
// error_log("Corrected layout SAVED to user meta - moved groups to normal area");
return $corrected_value;
}
// No misplaced groups - return saved layout as-is
return $value;
}
// No saved layout - apply defaults
if (empty($acf_boxes_for_normal)) {
return $value;
}
$normal = array_merge(array('acf_custom_acf_spacer'), $acf_boxes_for_normal);
return array(
'normal' => implode(',', $normal),
'side' => '',
'advanced' => '',
);
}, 10, 1);
}
/**
* Minimal admin CSS to hide the spacer meta box chrome while keeping the drop zone.
* Scoped to post edit screens so it doesn’t affect other admin pages.
*/
add_action('admin_head', function () {
// Only style the classic meta boxes area on post editor screens
if (!function_exists('get_current_screen')) {
return;
}
$screen = get_current_screen();
if (!is_object($screen) || $screen->base !== 'post') {
return;
}
echo '<style id="acf_custom-acf-spacer-css">'
. '#poststuff #acf_custom_acf_spacer.postbox{border:0;box-shadow:none;background:transparent;padding:0;margin:0 0 12px 0;}'
. '#poststuff #acf_custom_acf_spacer .postbox-header,'
. '#poststuff #acf_custom_acf_spacer .hndle,'
. '#poststuff #acf_custom_acf_spacer .handle-actions{display:none;}'
. '#poststuff #acf_custom_acf_spacer .inside{padding:0;margin:0;}'
. '</style>';
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment