sveltekit

Hydration failed because the initial UI does not match what was rendered on the server


"@sveltejs/kit": "^2.34.0"

I am adding a close button to the svelte shadcn Tabs component and get this error. I have seen many questions on this in other frameworks but not sveltkit. If I do not add a button to the code it works, but as soon as I add a button I get the error

<Tabs.Root bind:value={activeTab}>
    <Tabs.List>
        {#each tabs as tab (tab.value)}
            <Tabs.Trigger value={tab.value}>
                {tab.label}
                <Button variant="ghost" size="sm" class="ml-2" onclick={(e) => { e.stopPropagation(); closeTab(tab.value); }}>x</Button>  <!--Error caused from adding button-->
            </Tabs.Trigger>
        {/each}
    </Tabs.List>
    {#each tabs as tab (tab.value)}
        <Tabs.Content value={tab.value}>
            <p>Content for {tab.label}</p>
        </Tabs.Content>
    {/each}
</Tabs.Root>

You can see where I add the button.

  1. What does this error really mean?

To me what I am doing is something very simple and should not cause any errors. I am new to sveltekit and ssr but this is very frustrating.


Solution

  • Server-side rendering generates HTML which does not contain any JS-based interactivity, for that hydration has to happen on the client. To find the existing HTML elements, the Svelte code walks the DOM element hierarchy to find the correct elements to attach event handlers and other client-side logic to.

    If the server generates HTML which is not valid, the browser will try to fix it, creating a different structure than what the code expects. This will raise hydration errors.

    The problem here is most likely that you cannot have a button within another button, e.g. this code:

    <button>
        A
        <button>B</button>
    </button>
    

    Will cause the browser to close the outer button when the inner button starts, leading to:

    <button>A</button>
    <button>B</button>
    

    Svelte in this case would look for a child button of button A and not find any.

    The tab triggers already contain buttons by default as far as I can tell, so you cannot add another inside them.

    It looks like you could set a child snippet in the tabs Trigger component from bits-ui, that is used by default, to completely control the contents (source). This way you can prevent the nesting of buttons.

    In general, you can always get the raw HTML that is being sent from the server and validate it via one of the various HTML validity checkers.