Created
November 4, 2025 20:41
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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"> </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