-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change body class via <svelte:body /> #3105
Comments
I've considered this before - what should happen with this when compiling for SSR? There's nowhere in the current |
Seems, we already have something similar with const { head, html, css, body } = App.render({ ... }); And can be used in Sapper like this: <head>
...
%sapper.head%
</head>
<body %sapper.body%>
</body> I really believe the body class for pages is a common case for many apps. |
In case someone wants to change the class of the body it's very easy. Add this to the script part of your main layout file:
Then in a global css file like public/global.css add the styles for .my-class |
@jorgegorka Sorry, but everyone knows that. Here we're talking about a universal (client and server) and declarative way to set body classes. The way you described won't work with SSR and not declarative. |
The same kind of thing would be very useful for setting attributes on |
vote positive on <svelte:body class:name={confition} is usefull |
+1 It would be great if we could set it like this, from the special component and with SSR. |
+1 for <svelte:body class:name={confition}> as well as <svelte:html lang={lang}> |
If you want to change the whole background without having the content being taller than the fold, being able to change body class is pretty vital. Is there currently a workaround? If I'm not wrong, using I want to give body different classes based on which page is active. |
BTW, I think this feature might somewhat relate to sveltejs/sapper#374. That issue is basically asking a way for svelte to render directly at the html/body level, instead of a child node, and that probably affects how components can manipulate {body, html} {classes,attributes} |
I don't know what is the status of
|
Simple theme chooser. <script>
const themes = ['', 'dark-theme', 'light-theme']
const icons = ['🌙', '🌞', '⭐']
let index = 0
</script>
<button on:click={() => { index = (index + 1) % themes.length }}>{icons[index]}</button>
<svelte:body class={themes[index]}/>
<svelte:head>
<style>
/* CSS file */
:root {
--background-color: #a19585;
--text-color: #f7f7f7;
--primal-color: #30a5a7;
}
.light-theme {
--background-color: #fffaf4;
--text-color: #201f20;
}
.dark-theme {
--background-color: #201f20;
--text-color: #fffaf4;
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
</style>
</svelte:head> |
@Conduitry @Rich-Harris Any comments here? Seems this proposal is very popular. |
This comment has been minimized.
This comment has been minimized.
i have the same issue |
This comment has been minimized.
This comment has been minimized.
i also find that dynamic class assignments to the body element are really needed, especially in the sapper environment with SSR |
where is the solution? Someone knows? |
I want this feature alongside <svelte:html /> . On server side it might look like this: const { head, html, css, htmlAttrs, bodyAttrs } = App.render({ ... }) Sapper template: <html %sapper.htmlAttrs%>
<head>
...
%sapper.head%
</head>
<body %sapper.bodyAttrs%>
...
</body>
</html> |
This comment has been minimized.
This comment has been minimized.
+1 <script>
import { stores } from '@sapper/app';
import { onMount } from 'svelte';
const { session } = stores();
let root;
onMount( () => {
root = document.documentElement;
}
$: root && (hasUser => root.classList.toggle('loggedin', hasUser))(!!$session.user);
</script> |
This comment has been minimized.
This comment has been minimized.
Well I think it would be very helpful to be able to change a class or any other attribute on body simply by using |
This is what I was looking for. Clearly many common use cases for changing body class or variables. This needs to be solved in core |
Would really wish this would be implemented, but in the meantime, I used something similar to what @vegardlarsen has implemented. This is my utility function
This is called directly in the +page.svelte.
|
how do you import the class here? I have 2 css files, one for darkmode one for lightmode, currently I import 2 stylesheets. how do you transform stylesheet to html class? |
Chiming in here with my particular use case: I have a similar need, but with data-* attributes instead of CSS classes. Namely, I'd like to do something like the following (in my SvelteKit app): <!-- +layout.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<svelte:body data-page-id={$page.data.id} /> So I think if a solution to this issue is implemented, it should work for any attribute that can be applied to the |
Even a non-dynamic |
Thanks for trying to help, but that's not useful for this use case. What is needed is a server-side method to put a class attribute on the |
I'm not against adding this potentially great feature, but as it was already mentioned once, it seems that you can use the const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', locale), // assume locale is 'en'
}); The |
I don't think anyone is disputing the existence of work-arounds, but it would still be nice to get rid of all that boilerplate that is currently required for something that is extremely common. Your example only shows a small part of what is required to get a functional dark theme switcher to work. You also have to force a full refresh from the server after changing the value or dynamically modify the page in-browser while handling all edge cases and being careful not to disrupt the rest of the state. @iolyd gave a more complete example of what it takes. It is a great strategy. No doubt about that. But all that boilerplate could be replaced with something as simple as |
I'm siding with @FlippingBinary and the voices claiming handling Nevertheless, for posterity on my example, here's an updated / cleaned up implementation using a data attribute to foresee a cleaner handling in case I were to use this approach for multiple attributes. Feel free to adapt it to your use case: <!--
@component
# Root Theme
This singleton component manages the theme class applied to the `:root` element of the app.html.
The theme can be updated by the client using the global theme store.
Percolation of theme updates to the theme cookie can also be enabled/disabled for each use cases.
-->
<script lang="ts" context="module">
import { browser } from '$app/environment';
import { COOKIES } from '$utils/enums';
import { THEMES, type ThemeName } from '$utils/themes';
import type { RequestEvent } from '@sveltejs/kit';
import jscookie from 'js-cookie';
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
/**
* Use this theme to initialize the first SSR result inside hooks.
*/
export let defaultTheme: ThemeName = THEMES.light;
const COOKIE_LIFETIME = 60 * 60 * 24 * 365;
const ROOT = browser ? document.documentElement : undefined;
function setUserTheme(name: ThemeName | null) {
if (browser) {
if (name) {
jscookie.set(COOKIES.THEME, name, {
path: '/',
expires: Date.now() + COOKIE_LIFETIME,
});
} else {
jscookie.remove(COOKIES.THEME, { path: '/' });
}
}
}
export function getUserTheme(event?: RequestEvent) {
const cookie = event
? event.cookies.get(COOKIES.THEME)
: browser
? jscookie.get(COOKIES.THEME)
: undefined;
if (!cookie || !(THEMES as any)[cookie]) {
return defaultTheme;
}
return cookie as ThemeName;
}
const init = getUserTheme();
export const rootTheme = (function () {
const { subscribe, set: _set } = writable(init);
function set(name: ThemeName, setCookie: boolean = false) {
if (ROOT) {
ROOT.setAttribute('data-theme', name);
}
if (setCookie) {
setUserTheme(name);
}
_set(name);
}
function reset(setCookie: boolean = false) {
set(init, setCookie);
}
return {
subscribe,
reset,
set,
};
})();
</script>
<script lang="ts">
/**
* Set a root theme and reset to default theme following the lifecycle this component's instance.
*/
export let theme: ThemeName;
onMount(() => {
rootTheme.set(theme);
});
onDestroy(() => {
rootTheme.reset();
});
</script> // hooks/server.ts
export const handle: Handle = async ({ event, resolve }) => {
// ...
const theme = THEMES[getUserTheme(event)];
const res = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%app.theme%', theme),
});
return res;
}; @cyborgdennett My themes are generated as a series of It generates a simple stylesheet like so: [data-theme='light'] {
--color-bg-100: hsl(55, 20%, 97%);
...
--rgb-bg-100: 249, 249, 246;
...
}
[data-theme='dark'] {
...
} After that, its just a question of importing that stylesheet in your root |
I made a small SvelteKit library to address this and add a make-shift <ska:html lang="en" on:keyup={onHtmlKeyup} />
<svelte:body class:dark={darkModeEnabled} /> Not as good as first-class support in Svelte core obviously, but it should help. |
@barvian thanks for the library, really hoping this gets considered for core sveltekit. |
it would nice to have this feature however there is some issue might need to discuss before even start implementing it <!-- Overlay.svelte -->
<script lang="ts">
export let show = false;
</script>
<svelte:body class:overlay={show} />
<slot/>
<style>
:global(body.overlay) {
overflow: none !important;
}
</style> <!-- App.svelte -->
<script lang="ts">
import Overlay from './Overlay.svelte';
</script>
<Overlay show={true}/>
<Overlay show={false}/> we can see a clear conflict in this case |
@eslym that looks to me like it should be handled the same as duplicate CSS rules. The last one wins. However, maybe both could be output as CSS by the bundler in the order they appear so the browser can make that decision. Likewise, if a nested component and its parent both set a body property, both rules could be included in the output CSS so the browser can decide. The order they appear should be based on the order they naturally would be processed by the Svelte compiler so this contradictory use-case does not add much processing overhead. The documentation could note that the order in which duplicate rules are output and which rule takes precedence is not guaranteed. |
Little workaround I used, might be useful. <script>
let body;
let darkMode = false;
const bindBody = (node) => (body = node);
const setDarkClasses = (...classes) => {
if (!body) return;
if (darkMode) {
body.classList.add(...classes);
} else {
body.classList.remove(...classes);
}
};
$: {
darkMode;
setDarkClasses('class1', 'class2', 'class3');
}
</script>
<svelte:body use:bindBody />
<button on:click={() => (darkMode = !darkMode)}>Dark Mode</button>
|
@wistrix This is a workaround but would result in the page flickering due to this being rendered in the client side. We are looking for something that's ssr friendly. |
I have a svelte-body utility library for this purpose - hopefully we can get this in svelte soon though! |
does it add the class name when the page is rendering in the backend? |
Looking at the package shared by @ghostdevv, no this does not apply to SSR (it's an action-based approach and actions are browser-only since they bind to DOM nodes) SSR-friendly apparoaches need to provide some Look into @barvian's package, its the most complete and cleanest solution for now. |
I made a library svelte-attr that can dynamically change the attributes of html and body tags that also works with SSR. <HtmlAttr lang="en" />
<BodyAttr data-theme="dark" /> This library does not support the |
Interesting implementation, but basically it’s not equivalent solution, because your approach is mostly client-side and actual attributes doesn’t applied to the body/html tags during SSR. Anyway, this issue doesn’t have user-land solution and should be built-in to the Svelte itself. |
My problem was that I needed to set the theme colors in the backend to prevent that annoying flicker. So, in theory you can set the body variable and other styles during the SSR phase by using the :global() attribute. To implement this solution, I decided to set the classname of an html div element (the class of the div gets set in the backend rendering phase.) <script lang="ts">
export let data;
let theme = data.theme; // "lightTheme" | "darkTheme" | "systemTheme"
</script>
<div id="themeSetter" class="{theme}" /> Then, in the style section, I use the css :has() attribute, also the :global() attribute that svelte provides to set the body styles and variables. <style>
:global(body):has(#themeSetter.lightTheme) {
--text: #131615;
--background: #f6f9f7;
--primary: #59ab7e;
--secondary: #97d8b5;
--accent: #69d89d;
}
</style> You can go even more in depth: <style>
@media (prefers-color-scheme: light) {
:global(body):has(#themeSetter.systemTheme) {
--text: #131615;
--background: #f6f9f7;
--primary: #59ab7e;
--secondary: #97d8b5;
--accent: #69d89d;
}
}
@media (prefers-color-scheme: dark) {
:global(body):has(#themeSetter.systemTheme) {
--text: #e9eceb;
--background: #060907;
--primary: #54a679;
--secondary: #276845;
--accent: #27965b;
}
}
:global(body):has(#themeSetter.darkTheme) {
--text: #e9eceb;
--background: #060907;
--primary: #54a679;
--secondary: #276845;
--accent: #27965b;
}
:global(body):has(#themeSetter.lightTheme) {
--text: #131615;
--background: #f6f9f7;
--primary: #59ab7e;
--secondary: #97d8b5;
--accent: #69d89d;
}
</style> This solution worked for me. The :has() attribute seems to be supported by all the major browsers. The flicker no longer exists, and no javascript is required in the frontend to set the color variables because everything is happening in the backend. |
The only problem is that you're not setting the class name in the body tag. But by setting the class of an html div element and using the :global() and :has() attributes, you are essentially setting the classname for the body. Also you can change the classname in the frontend without any reloads. <script lang="ts">
export let data;
let theme = data.theme;
function themeChange(event) {
document.cookie = `theme=${event.target.value}; expires=Thu, 18 Dec 2030 12:00:00 UTC`;
theme = event.target.value;
}
</script>
<div id="themeSetter" class="{theme}" />
<select on:change={themeChange}>
<!-- excuse the ugly code -->
<option value="darkTheme" selected={data.theme==="darkTheme"}>Dark</option>
<option value="lightTheme" selected={data.theme==="lightTheme"}>Light</option>
<option value="systemTheme" selected={data.theme==="systemTheme"}>System</option>
</select> |
I'm solving this issue using a custom action that set the class(es) I need on the body. classList.tsimport type { Action } from "svelte/action";
export const classList: Action<Element, string | string[]> = (node, classes) => {
const tokens = Array.isArray(classes) ? classes : [classes];
node.classList.add(...tokens);
return {
destroy() {
node.classList.remove(...tokens);
},
};
}; +page.svelte<script lang="ts">
import { classList } from "$lib/actions/classList";
</script>
<svelte:body use:classList={"bg-gray-50"} /> |
I guess this is a difficult feature to implement since there is no official solution from the team. It seems to be a very popular request for a long time, and ultimately it's a really useful and reasonable request to simply change classes and other attributes for I really hope this can be added to the stable version of Svelte 5. As already suggested above (#1, #2), here is an example that works well for my use case. Also this #3 will not work in SvelteKit since I needed to change some styles only for a specific page. This is a workaround when it comes to styles. First, let's set the derived <!-- +layout.svelte -->
<script lang="ts">
import { page } from '$app/stores'
import '../styles/main.css'
let { children } = $props()
// optional parsing step for friendly string output
function parsePath(path: string) {
let newPath: string = ''
if (path === '/') newPath = '/home'
else newPath = path
// replaces route `/` with `dash`
return newPath.replace(/\//g, '-').slice(1)
}
let pageId = $derived(parsePath($page.url.pathname))
</script>
<!-- automatically sets `page-id` on each route change -->
<div data-page="{pageId}">{@render children()}</div> After that you can easily add styles for /* styles/main.css */
/* html styles for 'home' page */
html:has(div[data-page='home']) {
background: blue;
}
/* body styles for 'about' page */
body:has(div[data-page='about']) {
background: green;
}
/* etc... */ |
It's just an idea, but it'll be very convenient if we'll able to switch classes on body element like this:
The text was updated successfully, but these errors were encountered: