Bug
Every page reload with reactive conditional directives (:if, :else, :else-if, x-if, x-else, x-else-if, :show, x-show) flashes false-branch / hidden content before the runtime hides it. The intent is x-cloak should fix this, but the current implementation can't — and even if it could, the per-element opt-in is a recurring footgun.
Repro
Any template with a reactive :if chain:
<script client>
const isAuthenticated = derived<boolean>(() => !!token())
</script>
<div :if="isAuthenticated()">Welcome back</div>
<div :else>
<a href="/login">Sign in</a> to continue
</div>Hard-reload the page (incognito tab, throttled 3G in DevTools). The Welcome banner AND the Sign-in banner both render for ~100-300ms before only the active one stays. Same for modals (:if="modalOpen()" flashing open on mount), skeleton loaders (:if="!loaded()" flashing alongside the loaded content), bell badges, etc.
Adding x-cloak to every conditional element doesn't fix it. I tested with a clean template, sprinkled x-cloak on the :if/:else divs — same flash.
Root cause
signals.ts (line 115-118 in current main) injects the cloak rule inside the runtime script:
// Inject x-cloak CSS to prevent FOUC (Flash of Unstyled Content)
// Elements with x-cloak are hidden until the runtime removes the attribute after mount
const cloakStyle = document.createElement('style')
cloakStyle.textContent = '[x-cloak] { display: none !important; }'
document.head.appendChild(cloakStyle)Page-load timeline:
- Browser parses HTML, applies inline
<style>blocks in<head>(crosswind preflight + utilities) - Browser paints body — all conditional elements are visible,
x-cloakdoes nothing because no rule for it has been parsed yet - JS runs → runtime executes →
cloakStyleappended to head → browser applies the rule - Runtime processes
:if/:elsechains → removes attributes from active branch, detaches false branches - User sees the correct UI
The flash is the window between (2) and (3-4). x-cloak can't help because step (3) is where its rule gets defined. Telling adopters to "use x-cloak to prevent FOUC" is misleading — by the time x-cloak's CSS exists, the FOUC has already happened.
Why the per-element opt-in is the wrong shape
Even if the rule shipped early enough, requiring x-cloak on every conditional element is:
- Easy to forget — every new
:ifis a fresh chance to ship an unintended flash. The framework knows where the directives are; the dev shouldn't have to. - Inconsistent with the rest of the directive family —
:ifalready auto-handles detachment, cleanup, scope disposal. Asking the dev to add a separatex-cloakattribute alongside breaks the "directives are declarative" promise. - Multiplies with
#1734— the new:if/:else-if/:elsechains mean a single conditional often becomes 3+ elements. Three more places to forgetx-cloak.
The framework should opt-in by default and opt-out by exception, the same way React SSR / Vue SSR / Nuxt do.
Prior art
- React/Vue SSR (Next, Remix, Nuxt): Server evaluates the conditional at render and emits ONLY the active branch. Zero FOUC by construction because false branches never enter the DOM. This is the gold standard but requires expression analysis to know what's server-resolvable.
- Alpine.js (closest analog to stx's
x-directives): Ships the cloak rule in their docs as a one-liner you put in<head>once, then addx-cloakper-element. Same footgun as stx, just documented as "this is your responsibility." - Vue SFC
v-if/v-else: Compiles at build to a render function; the runtime conditionally invokes the branch. No flash because the framework owns the render path entirely.
Proposed fix (incremental, ships in one diff)
A. SSR injects the cloak rule into the generated <head>
generateSpaShell (or equivalent in bun-plugin-stx) emits a small <style> block as part of the head:
<style data-stx-cloak>
[x-cloak] { display: none !important; }
</style>Browser parses this with the rest of the head — rule is live by step (2) in the timeline above. No script execution required.
B. SSR auto-stamps x-cloak on every element with a reactive conditional directive
During the same pass that processes :if/x-if/etc., scan for these attributes on the element:
:if, :else, :else-if, :show, @if, @show
x-if, x-else, x-else-if, x-showIf any present, also add x-cloak. The runtime already strips x-cloak at the moment it processes the element (lines 2192-2193, 2200-2201, 2533-2534, 2666-2667, etc.) — no runtime change needed. The only change is at the SSR processor.
C. Compat
- Hand-written
x-cloakstill works (it's just additive — the SSR pass would set it if not already present). - Build-time
@if/