Skip to content

Instantly share code, notes, and snippets.

@rotimi-best
Created July 29, 2026 14:34
Show Gist options
  • Select an option

  • Save rotimi-best/534c50fa3faba78b1e7b255e0be6a461 to your computer and use it in GitHub Desktop.

Select an option

Save rotimi-best/534c50fa3faba78b1e7b255e0be6a461 to your computer and use it in GitHub Desktop.

svelte:body is a solid approach for this specific problem — making --landing-* available to portaled dialogs without touching every overlay.

What it fixes

Portaled content is still a descendant of <body>, so if the vars live on body, dialogs/sheets inherit them automatically:

<svelte:body style={themeStyle(theme)} />

You set it once per landing page (ideally in a shared wrapper), not on every Dialog.Content.

Recommended shape

Extract a small scope component instead of sprinkling this across 20 theme composers:

<!-- landing-theme-scope.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  import type { OrgLandingPageTheme } from './types';
  import { themeStyle } from './theme-style';

  let { theme, children }: { theme: OrgLandingPageTheme; children: Snippet } = $props();
</script>

<svelte:body style={themeStyle(theme)} data-landing-theme={theme} />

<div
  class="ui:min-h-screen ui:bg-[var(--landing-bg)] ui:text-[var(--landing-fg)]"
  style={themeStyle(theme)}
>
  {@render children()}
</div>

Keep style on the inner div too if you want the page to work even when something else temporarily owns body styles. Or drop the div style and rely on inheritance from body — either works.

Then each {theme}/org.svelte and {theme}/course.svelte wraps content in <LandingThemeScope {theme}> instead of a raw <div style={themeStyle(...)}>.

Why this is safe in your app

  • Namespaced vars--landing-* won't stomp dashboard --background / --foreground.
  • Auto cleanup — Svelte removes svelte:body attributes when the component unmounts, so navigating away from an org-site page or closing the editor preview clears them.
  • Editor preview — the landing editor mounts ThemeComponent inside the dashboard (edit/+page.svelte). Body-level --landing-* vars are fine there; they don't restyle the sidebar chrome.

What it does not fix

Base Dialog / Sheet still use dashboard tokens:

      'ui:bg-background ui:border ui:data-[state=open]:animate-in ...

So vars being available ≠ components using them. You still need one of:

  1. Global landing overlay styles (zero per-dialog work):
/* landing-overlays.css */
body[data-landing-theme] [data-slot='dialog-content'],
body[data-landing-theme] [data-slot='sheet-content'] {
  background: var(--landing-card);
  color: var(--landing-fg);
  border-color: var(--landing-border);
}
  1. One landing wrapper (LandingDialog.Content) if you want explicit control per overlay type.

I'd lean toward (1) with the data-landing-theme attribute — then any dialog/sheet you add "just works" as long as it uses the base primitives.

vs context / per-dialog style

Approach Vars on portaled content Per-dialog boilerplate
svelte:body + global overlay CSS None
Context + pass style to each dialog Every dialog
portalProps={{ to: root }} ✅ (if in subtree) Every dialog + stacking caveats

Bottom line

Use <svelte:body style={themeStyle(theme)} /> in a shared LandingThemeScope — that's the Svelte-native way to solve the portal inheritance problem once. Pair it with either global [data-slot=dialog-content] rules under body[data-landing-theme], or a single landing dialog/sheet wrapper for the class mapping.

If you want, I can add LandingThemeScope and wire it into the theme composers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment