svelte:body is a solid approach for this specific problem — making --landing-* available to portaled dialogs without touching every overlay.
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.
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(...)}>.
- Namespaced vars —
--landing-*won't stomp dashboard--background/--foreground. - Auto cleanup — Svelte removes
svelte:bodyattributes 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
ThemeComponentinside the dashboard (edit/+page.svelte). Body-level--landing-*vars are fine there; they don't restyle the sidebar chrome.
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:
- 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);
}- 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.
| 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 |
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.