修正 Elementor 導覽選單那一下「閃」:實測穩定解法分享

目錄

文章摘要:

在使用 Elementor 製作網站導覽選單時,常會出現頁面剛載入就閃爍或跳動的情況。選單項目會短暫位移,下拉箭頭稍後才出現,讓整個頁首顯得不穩定。問題並非源自 CSS 排版,而是 Elementor 的 Nav Menu 小工具在初始化時,透過 JavaScript 動態插入箭頭圖示所造成的結果。

當網站啟用了像 WP Rocket、LiteSpeed Cache 等快取外掛時,這種延遲更為明顯,因為外掛改變了 JavaScript 的執行時機,使箭頭在畫面顯示後才被補上。修正方式是讓箭頭在伺服器端(PHP)階段就先輸出到 HTML,並使用 CSS 隱藏 Elementor 原生插入的版本,讓畫面在首次渲染時就保持完整。

這個方法不需要修改 Elementor 核心,也不影響編輯器預覽。只需幾行程式,即可讓導覽列在載入時穩定呈現,不再閃爍或跳動,整體體驗更一致、更專業。

在使用 Elementor 的導覽選單製作網站頁首(Header)時,常會遇到一個令人在意的細節:網頁剛載入時,選單項目會突然「跳動」一下,然後才看到下拉箭頭出現。

這種畫面上的抖動不只是視覺干擾,它其實會讓使用者在第一眼就感覺到畫面的不穩定與不一致。雖然它不構成錯誤,也不會破壞功能,但在體驗上卻足以讓整體介面顯得不夠扎實。

特別是當網站啟用了像 WP Rocket、LiteSpeed Cache 等快取外掛時,問題會變得更加明顯。這些外掛通常會調整 JavaScript 的載入時機,或延後腳本執行,進而放大了這種「畫面先顯示、箭頭後補上」的時間差。

問題根源:下拉箭頭是由 JS 動態插入

Elementor 的 Nav Menu 元件,在前端初始化時會掃描所有有子選單的項目,然後用 JavaScript 動態插入箭頭圖示。這個插入動作通常是在網頁主體載入完成後進行,也就是說——HTML 一開始是不含這些箭頭的。

具體來說,Elementor 是在 element_ready 階段插入 <span class="sub-arrow">,這動作不大,但正好會觸發瀏覽器對 DOM 結構的重新排版(reflow)與重繪(repaint)。因此你會看到畫面明明已經排好,卻因為多了一個元素導致整列選單發生閃爍或跳位現象。

這種「先載入再插入」的策略也會與 CSS 的排版邏輯產生衝突,尤其你若使用的是 flexbox、inline-block 排版方式,或設計上使用了自訂的間距、字級、行高等控制,這個箭頭插入的延遲就會非常明顯,甚至影響整體版面穩定性。

解決方向:讓箭頭預先存在、並隱藏插入的箭頭

既然問題在於JS插入的箭頭出現得太晚,我們就反過來處理:讓箭頭隱藏起來。

更精確的說法是:

  1. 讓 Elementor 的箭頭在伺服器端(PHP)渲染階段就寫入 HTML。
  2. 同時隱藏JS插入的箭頭,讓視覺上不會因為晚插入的箭頭產生偏移閃爍。

這樣做可以確保兩件事:

  • 畫面第一次載入時,箭頭就已經在 DOM 結構中,就算因為新增節點而重繪,也早一先一步使用css將其隱藏。
  • Elementor 的其他功能(例如子選單展開收合)依然可以正常使用,因為我們只是隱藏了原本閃爍的箭頭,並沒有移除整體互動行為。

程式碼實作

這段程式碼可以直接放進主題的 functions.php,或打包成自訂外掛使用。

不過更建議的做法是,建立一個子佈景主題(Child Theme),將這類客製化功能集中管理。這樣不僅能讓代碼結構更清晰,也能避免日後主題更新時自訂修改被覆蓋或消失的情況。

/**
 * 修復 Elementor 導航選單(Nav Menu)下拉指標閃爍問題
 * ------------------------------------------------------------
 * - 攔截 Nav Menu widget 的 HTML 輸出
 * - 插入並修正子選單箭頭圖示位置與樣式
 * - 修正 SVG 顏色繼承、避免閃爍或重疊
 * - 僅前台生效(不影響後台與 Elementor 編輯器)
 */

add_filter('elementor/widget/render_content', function ($content, $widget) {
    if (is_admin() || (defined('ELEMENTOR_EDITOR') && ELEMENTOR_EDITOR)) {
        return $content;
    }
    if (!did_action('elementor/loaded')) {
        return $content;
    }
    if (!class_exists('\Elementor\Icons_Manager')) {
        return $content;
    }
    if ('nav-menu' !== $widget->get_name()) {
        return $content;
    }
    if (strpos($content, 'menu-item-has-children') === false) {
        return $content;
    }

    $icon = $widget->get_settings_for_display('submenu_icon');
    $icon_html = '';

    if (!empty($icon)) {
        if (is_array($icon)) {
            ob_start();
            \Elementor\Icons_Manager::render_icon($icon, ['aria-hidden' => 'true']);
            $rendered = ob_get_clean();

            $rendered = preg_replace('/fill="[^"]*"/i', '', $rendered);
            $rendered = preg_replace('/<svg(.*?)>/i', '<svg$1 fill="currentColor">', $rendered);

            $icon_html = '<span class="sub-arrow sub-arrow-icon">' . $rendered . '</span>';
        } elseif (is_string($icon)) {
            $icon_html = '<span class="sub-arrow sub-arrow-icon"><i class="' . esc_attr($icon) . '"></i></span>';
        }
    }

    if ($icon_html) {
        $content = preg_replace(
            '#(<li[^>]*class="[^"]*\bmenu-item-has-children\b[^"]*"[^>]*>\s*<a[^>]*?>)(.*?)(</a>)#si',
            '$1$2 ' . $icon_html . '$3',
            $content
        );
    }

    static $css_printed = false;
    if (!$css_printed) {
        add_action('wp_footer', function () {
            echo '<style id="pongo-navmenu-icon-css">
                .elementor-nav-menu .sub-arrow { display: none !important; }
                .elementor-nav-menu .sub-arrow.sub-arrow-icon {
                    display: inline-block !important;
                    vertical-align: middle;
                    color: inherit !important;
                    fill: currentColor !important;
                }
                .elementor-nav-menu .menu-item-has-children:hover .sub-arrow-icon,
                .elementor-nav-menu .menu-item-has-children:focus-within .sub-arrow-icon {
                    color: currentColor !important;
                    fill: currentColor !important;
                }
            </style>';
        });
        $css_printed = true;
    }

    return $content;
}, 10, 2);Code language: PHP (php)

完成上述修改後,網頁載入的穩定性會有明顯改善。最直接的體感是:導覽選單不再抖動,箭頭從一開始就存在於版面中,整體畫面不再因 DOM 被修改而重新排版,也不會因箭頭晚一步出現而產生錯位、重疊、或 hover 效果異常的問題。

此外,這個方法是使用 Elementor 官方所提供的過濾器來增加輸出一個箭頭,不修改 Elementor 核心、不破壞編輯器預覽,對未來的更新也保有良好相容性。

結語

Elementor 之所以選擇在載入後再插入箭頭圖示,是出於元件化架構與樣式彈性的考量。然而在實際運行中,這種設計卻為導覽列帶來了意料之外的副作用——畫面閃爍、位移、整體穩定性下降。

如果你也遇到這個情況,其實不用再懷疑是 CSS 排版出了問題,也不必為了避開它而關閉快取或調整 JS 延遲策略。只要幾行簡潔的程式,就能讓導覽列在載入時維持穩定、不跳、不抖,呈現出應有的設計品質與細節一致性。

常見問題:

Elementor 的導覽選單(Nav Menu)在前端初始化階段,會透過 JavaScript 動態插入下拉箭頭圖示。畫面第一次載入時 HTML 裡並沒有箭頭,等到 JS 執行後才新增節點,瀏覽器因此重新排版(reflow)與重繪(repaint),就造成肉眼可見的閃爍與跳動。
不是。這個現象與主題或 CSS 無關,即使使用 Elementor 官方主題也可能出現。根本原因在於 Elementor 的導覽選單初始化機制,而非樣式排版問題。
快取外掛會改變或延遲 JavaScript 的執行時機,使得箭頭插入的時間點更晚。當畫面已經渲染完成、JS 才動態修改 DOM 時,瀏覽器就會再重新排版,閃爍與位移的現象因此更明顯。
不需要。建議使用 PHP 在伺服器端預先輸出箭頭圖示,並以 CSS 隱藏 Elementor 原生版本。這樣既能避免畫面重排,又不會影響 Elementor 核心與後續更新。
不會。這個修正只針對箭頭顯示的時機與樣式進行調整,不會改變導覽列的互動邏輯。子選單開合、hover 效果等功能都能正常運作。
建議建立一個子佈景主題(Child Theme),將這類自訂代碼統一放在子主題中管理。這樣可以避免主題更新時原始檔被覆蓋,讓修正能長期維持並確保安全。
您可能感興趣