Next.js Discord

Discord Forum

Is it bad to make the html tag in layout.tsx a client component?

Unanswered
American Wirehair posted this in #help-forum
Open in Discord
American WirehairOP
I want to set a data-theme="red" property on the html tag, selected randomly on page load. Can i make a client component with the <html> tag, and do that there, or would that be a bad idea?

39 Replies

American WirehairOP
html.tsx
"use client";

import { useRef } from "react";

interface HtmlProps {
  className?: string;
  children?: React.ReactNode;
  lang?: string;
}

const themeValues = ["red", "blue"] as const;

export function Html(props: HtmlProps) {
  const theme = useRef(themeValues[Math.floor(Math.random() * themeValues.length)])
  return (
    <html data-theme={theme.current} lang={props.lang} className={props.className}>
      {props.children}
    </html>
  );
}

layout.tsx
  return (
    <Html
      lang="en"
      className={`${consort.variable} ${graebenbach.variable}`}
    >
      <body
        className={`bg-white font-sans theme-green:text-green theme-blue:text-blue theme-red:text-red theme-brown:text-brown`}
      >
        <Header theme={"brown"} globals={globals} />
        {children}
        <Footer globals={globals} />
      </body>
    </Html>
  );
}
Is it bad to make the html tag in layout.tsx a client component?
Yes, usually its not really a good practice to make layout/page.tsx a client component. it has shown to result in errors in the past.

i saw your code. im not sure why you need a client component for that.

do you want to create a static route or a dynamic route?

if dynamic route, dont need client component.

if static route, well, you can always display blank until the browser loaded the js and run your randomizer using useEffect that targets <html>
American WirehairOP
I'm trying to achieve "best of both worlds". Statically rendered website, but where the data-theme is changed on each full page reload, so that the site displays different theme/colors. Also I want to avoid having to set the theme using useEffect as not have the website flash a color change (from default color to the random color from useEffect).
well.. if you dont use useEffect then the theme will only get randomized once at build time :/
and re-randomized again at request-time
which would... also.. flash a color change
(from the default color to the random color from useRef re-running in client components)
soo.........
American WirehairOP
I could do a:
<head>
        <script dangerouslySetInnerHTML={{
          __html: `
            (function() {
              const themes = ["red", "blue", "brown", "green"];
              const theme = themes[Math.floor(Math.random() * themes.length)];
              document.documentElement.setAttribute('data-theme', theme);
            })();
          `
        }} />
      </head>


this will cause a hydration warning though.
these are all just the same idea and logic but different implementation
what are you trying to achieve?
why randomize theme?
American WirehairOP
That's the design from the agency I'm implementing. 🙂 They have various logo variants and color themes they want to show of. random on full page reload, but stable on client side navigation.
you can't avoid flash of white but you can avoid flash of color change
like i said, just display blank site if js is not loaded
or perhaps use PPR to have dynamic component in a static route
so that randomization happens on the server not on the client
if randomization happens on the server there would be no flash of anything
American WirehairOP
the script tag i posted above won't flash, as it will put the theme there on first render. But will create the hydration warning.

Current best solution is probably to make the entire site dynamic.
what is PPR?
@American Wirehair what is PPR?
partial pre-rendering, as much as the name not self-describe, it just allows you to add dynamic components while making the entire thing static
it needs you to use experimenta.ppr: true or cacheComponent: true
American WirehairOP
ohh, cool. I'll have to read up about that. Might be a solution to to cache stuff below it.
cache stuff above it*
i was thinking of just plopping <RandomizedTheme /> in the children of <body> (not affecting the rest of the page)
then just use CSS selector to select the theme from html
that way you dont need to rely on data-theme no more
American WirehairOP
Hmm, I have to test that out, and see if I fully understand it, and how it will work. 🙂

Another issue then is that I currently pass the theme as a prop to Header to conditionally render different svg logos.
  const themes = ["red", "blue", "green", "brown"] as const;
  const theme = themes[Math.floor(Math.random() * themes.length)] || "blue";

  return (
    <html
      lang="no"
      data-theme={theme}
      className={`${consort.variable} ${graebenbach.variable}`}
    >
      <body
      >
        <Header theme={theme} globals={globals} />


I could hide them based on the same css selectors, but the logos have to be inline svg, and then all of them will be included in the html. Which is a lot!
header.tsx
interface HeaderProps {
  globals: Globals;
  theme: "brown" | "blue" | "red" | "green";
  className?: string;
}

export function Header(props: HeaderProps) {
  return (
    <header className={clsx(props.className, "border-b")}>
      <div className="h-32 py-4 lg:h-64 lg:py-8 flex items-center justify-center">
        {Match.value(props.theme).pipe(
          Match.when("brown", () => (
            <>
              <PALBrownAlt className="hidden lg:block" />
              <PALBrownAltMobile className="lg:hidden" />
            </>
          )),
          Match.when("blue", () => (
            <>
              <PALBlue className="hidden lg:block" />
              <PALBlueMobile className="lg:hidden" />
            </>
          )),
          Match.when("red", () => (
            <>
              <PALRed className="hidden lg:block" />
              <PALRedMobile className="lg:hidden" />
            </>
          )),
          Match.when("green", () => (
            <>
              <PALGreenAlt className="hidden lg:block" />
              <PALGreenAltMobile className="lg:hidden" />
            </>
          )),
          Match.exhaustive,
        )}
      </div>
interesting. if you dont need to have the <svg> interactive, you can sandbox it and lazy load it using <img>. that way you wont be penalized for writing many svg at once
American WirehairOP
they use the currentColor for fill. That's why they are inlined. 🙂 But that might not be needed.
interesting.
for sake of simplicity, why not just use client component and do opacity-0 initially and transition to opacity-100 when js, and theme is loaded
American WirehairOP
Yeah, I could. Just not a fan of that. 😛 I'm researching all possibilities before I either do that, or forced-dynamic.
not recommended to do forced-dynamic :(
assuming ure in next15, just write await cookies() or something lol
American WirehairOP
didn't know about the next-themes. I might look at that for inspo. Yeah, I see they just supress the hydration warning. 🙂
Ok, I have quite a few options to look into. 😄
1. Cache Components: https://nextjs.org/docs/app/getting-started/cache-components
2. next-themes (just supress hydration warning and set the theme in a head script)
3. opt out of static generation
4. fade in page once js is loaded and theme is set

Thanks a lot for the chat and help. 🙂
anytime!