Client component hydration has been a trendy topic these days. Some frameworks like Next, Astro and others are already doing it.
It is not common knowledge that we can achieve a very similar result with pure SSR and declarative shadow DOM.
Here’s what we will achieve, with examples:
Here are two buttons—both are created from a server rendered HTML structure that is automatically hydrated on the client with the client component. Watch the console while you click them, you’ll see a brief message logged each time you click any of them.
I am using a feature called Declarative Shadow DOM to make this happen. Here’s more details on this feature from the Chrome team.
Practically, we can now define a shadow DOM that will be rendered by the browser at render and on top of that it will also automatically upgrade that to a client web component, assuming we have defined and declared that appropriately.
In order to be able to hydrate automatically, the browser need an actual client component. Here’s how this component looks like in the case of the blue button, which I called PrimaryButton
class PrimaryButton extends HTMLElement {
constructor() {
super();
this.shadowRoot.querySelector("button").removeAttribute("disabled");
this.shadowRoot.querySelector("button").addEventListener("click", e => {
e.preventDefault();
console.log("button clicked");
});
}
}
customElements.define("primary-button", PrimaryButton);
Not much happening here: I declare the class and in its constructor I call the super()
, remove the disabled
attribute and then I attach a click
event for demo purposes.
And here’s the magic: when the browser is parsing the HTML and it has this client component available, it will automatically hydrate the server rendered HTML with this logic—we literally don’t need to do anything for this to happen, only have the server rendered structure in place. Let’s have a look at that.
This is nothing else but a fancy name for the html from the server, whic looks like this in HTML as a Go template:
<primary-button>
{{template "button" "primary"}}
Primary
</primary-button>
{{define "button"}}
<template shadowrootmode="open">
<link rel="stylesheet" href="/public/css/button.css" />
<link rel="stylesheet" href="/public/css/{{.}}-button.css" />
<button disabled>
<slot<>/slot>
</button>
</template>
{{end}}
First one is the usage of the web component bound to a custom element, the second one is the reusable part for the declarative DOM shadow itself.
The second one also helps me pass in a parameter to load a different CSS specific for that particular component.
The more general button CSS:
button {
color: var(--color);
background-color: var(--background-color);
font-size: var(--font-size);
padding: .5em 1em;
border: 1px solid var(--color-neutral-1);
border-radius: var(--space-l);
cursor: pointer;
}
button[disabled] {
opacity: .5;
cursor: not-allowed;
}
And the more specific button CSS, which just overrides the CSS vars I want to use for styling:
button {
--color: var(--color-neutral-1);
--background-color: var(--color-brand);
--font-size: var(--step-1);
}
Important note: not all CSS leaks through the shadow barrier. Here’s the list of CSS properties that do:
1. border-collapse 13. font-weight 25. text-align
2. border-spacing 14. font-size-adjust 26. text-align-last
3. caption-side 15. font-stretch 27. text-decoration-color
4. color 16. letter-spacing 28. text-indent
5. cursor 17. line-height 29. text-justify
6. direction 18. list-style 30. text-shadow
7. empty-cells 19. list-style-image 31. text-transform
8. font 20. list-style-position 32. white-space widows
9. font-family 21. list-style-type 33. word-break
10. font-size 22. orphans 34. word-spacing
11. font-style 23. quotes 35. word-wrap
12. font-variant 24. tab-size 36. visibility
This is why I have used a different approach for styling the buttons as opposed to inheriting through the traditional cascade.
One minor challenge that I have right now is that Firefox doesn’t yet support declarative shadow DOM, so there is a shim in the source of this page to make it all work in Firefox. Hopefully they will implement soon and I can update the post.
We can use SSR generated web components with automatic hydration in the browsers right now, without any dependencies by implementing declarative DOM shadows and their counterpart web components. Absolutely no framework needed to achieve this.