Fixed Notes
← All posts

The APEX navigation that jumped on every page load

Moustafa Abdelsalam · · 6 min read

Fix Oracle APEX Tested on APEX 26.1.0universal themepage templatejavascriptdynamic actionflicker

On this page5 sections
  1. The cause: JavaScript moving the navigation after the page has rendered
  2. How to prove it: render the page with scripts turned off
  3. The fix: render it in the right place to begin with
  4. If you can’t change the template
  5. Rules I now follow

Every time a user moved between pages of an APEX application, the navigation tabs appeared for a split second at the top of the header, next to the logo, and then jumped down into the tab row where they belonged. It looked like a glitch, and it happened on every single page.

Nothing was broken in the usual sense: no error, no console message, and the tabs ended up in the right place. But it made the application feel unfinished, and users noticed.

The cause: JavaScript moving the navigation after the page has rendered#

The header of a Universal Theme page template has two rows. The page template decides which component goes where. In this application, the template rendered the navigation bar list in the top row, next to the logo:

<header class="t-Header" id="t_Header">
  <div class="t-Header-branding">
    <div class="t-Header-logo">...#LOGO#...</div>
    <div class="t-Header-navBar">
      <div class="t-Header-navBar--center">#NAVIGATION_BAR#</div>
    </div>
  </div>
  <div class="t-Header-nav">#TOP_GLOBAL_NAVIGATION_LIST#</div>
</header>

The design wanted the tabs in the second row, so someone had added a dynamic action on page 0 (the global page), on Page Load, that moved them:

var navBar = document.querySelector('#t_Header .t-Header-navBar');
var navRow = document.querySelector('#t_Header .t-Header-nav');
if (navBar && navRow && !navRow.contains(navBar)) {
  navRow.appendChild(navBar);
}

The code is correct, and that is why the cause was hard to see. The problem is when it runs. A Page Load dynamic action runs after the page has been parsed. By then the browser has usually already drawn the page at least once, with the tabs where the template put them. So the user sees two layouts in a row: the server’s, then the script’s. That second, late change is the jump.

The same applies to anything a Page Load script creates or moves: a button injected into the header, a region moved into another position, a label rewritten. Each of them can appear in its original state first.

How to prove it: render the page with scripts turned off#

Reading the code tells you what the script does. It doesn’t tell you what the user saw before it ran. You can show that directly: fetch the page’s HTML and render it in a hidden iframe without permission to run scripts. What you get is the page as the browser draws it before any JavaScript runs.

Run this in the browser console on the affected page:

(async () => {
  const html = await (await fetch(location.href, { cache: 'reload' })).text();
  const f = document.createElement('iframe');
  f.setAttribute('sandbox', 'allow-same-origin');   // no allow-scripts: nothing runs
  f.style.cssText = 'position:fixed;left:0;top:0;width:' + innerWidth +
                    'px;height:600px;opacity:0;pointer-events:none';
  document.body.appendChild(f);
  f.srcdoc = html.replace('<head>', '<head><base href="' + location.href + '">');
  await new Promise(r => f.onload = r);
  await new Promise(r => setTimeout(r, 1500));       // give the stylesheets a moment

  const before = f.contentDocument.querySelector('#t_Header .t-Header-navBar');
  const after  = document.querySelector('#t_Header .t-Header-navBar');
  console.log('before scripts:', before.parentElement.className);
  console.log('after scripts: ', after.parentElement.className);
  f.remove();
})();

Two details make it work:

  • The sandbox attribute without allow-scripts stops every script in the frame, both APEX’s own and yours. allow-same-origin keeps the frame on the same origin, so the console can read its document.
  • The <base href> makes the page’s relative links (stylesheets, images) resolve as they do on the real page. Without it, the frame renders unstyled.

In my case it printed:

before scripts: t-Header-branding
after scripts:  t-Header-nav

That is the whole bug in two lines. Before the script runs, the tabs sit in the top row (t-Header-branding). After it runs, they are in the tab row (t-Header-nav). The user sees the first state, then the second.

Change the selector to check anything else your page scripts move or create. For an element that a script creates, the “before” lookup returns nothing, which is the same finding: the element only exists after the script runs, so it always appears late.

The fix: render it in the right place to begin with#

If the server sends the HTML with the tabs already in the tab row, there is nothing to move and nothing to see. So fix the page template, not the page.

In App Builder, open Shared Components > Templates, open the page template your pages use, and in Definition > Header move the navigation bar block into the second row:

<header class="t-Header" id="t_Header">
  <div class="t-Header-branding">
    <div class="t-Header-logo">...#LOGO#...</div>
  </div>
  <div class="t-Header-nav">#TOP_GLOBAL_NAVIGATION_LIST#
    <div class="t-Header-navBar">
      <div class="t-Header-navBar--center">#NAVIGATION_BAR#</div>
    </div>
  </div>
</header>

Things to check before and after:

  • Change every template your pages use. An application often has more than one page template, for example a standard one and a copy someone made for a few pages. Check which template each page uses (Page > Appearance > Page Template, or the PAGE_TEMPLATE column of APEX_APPLICATION_PAGES). If the application is translated, check the translated application too after you publish it.
  • Keep the CSS selectors working. The tabs now live inside .t-Header-nav from the start, which is exactly where the script used to put them, so CSS written for the moved version still applies.
  • The old script can stay or go. Because of its !navRow.contains(navBar) guard, it now finds the tabs already in place and does nothing. Removing it is cleaner. Leaving it is harmless.
  • Theme Refresh will undo this. Refreshing the theme replaces the Universal Theme templates with the theme’s own versions, which puts the tabs back in the top row and brings the jump back. Note the change somewhere your team will see it, or work in a copy of the template.

After the change, the same console check printed t-Header-nav for both lines: the tabs are in the tab row before any script runs, and the jump was gone.

If you can’t change the template#

Sometimes the template is shared or locked, and changing it is not your call. You can’t remove the late move then, but you can stop the user from seeing the first state. Hide the element in its original position with CSS, which applies on the very first render:

/* the tabs are invisible until the script has moved them */
#t_Header .t-Header-branding .t-Header-navBar { visibility: hidden; }

Once the script moves the element into .t-Header-nav, this rule no longer matches and the tabs appear. The row is empty for a moment instead of showing the tabs in the wrong place, which is much less noticeable. Use visibility: hidden rather than display: none if the element’s size affects the layout around it.

This is a workaround, and unlike the template change I haven’t tested it myself. The template fix is the real one, because it also removes the extra work on every page load.

Rules I now follow#

  • Page Load JavaScript runs after the first render. Anything it moves or creates can be seen in its original state first. Keep layout decisions in the template or in region positions, and use page JavaScript for behaviour.
  • Prove what the user sees before scripts run. A sandboxed iframe without allow-scripts renders the server’s HTML exactly as the browser receives it.
  • Fix it where the HTML is made. If something is in the wrong place, put it in the right place in the template rather than moving it in the browser.
  • Write down template changes. A Theme Refresh replaces them without warning.