Use the native Web Component Shadow DOM API declaratively in React.
Check out the demo and code for it!
- 🗜️ 2.8k
- 🌲 Browser-native CSS scoping.
- ✍️ Write your CSS with strings, objects or functions that return either.
- 🥤 Works with Shadow DOM polyfill.
npm install react-shade
This library exposes the W3C standardised Web Component APIs for Shadow DOM as a set of React components. This side-steps a lot of work that needs to be done to work with the imperative APIs - as well as the proposed declarative APIs - and gives you a nice, clean and simple interface to encapsulate your DOM / styles in a perfectly declarative manner.
import React from "react";
import { render } from "react-dom";
import Root, { Slot, Style } from "react-shade";
const App = () => (
<Root>
<Style>
{{
".totes-not-global": {
fontWeight: "bold"
}
}}
</Style>
<span className="totes-not-global">This will be bold.</span>
<Slot>
<span className="totes-not-global">This will NOT be bold</span>
</Slot>
</Root>
);
render(<App />, window.root);
This will produce something like:
<!-- Requires a wrapping node because it needs to have a node to attach
the shadow root to. -->
<div>
<!-- This is where the slot content ends up (as light DOM). -->
<span class="totes-not-global" slot="slot-0">This will NOT be bold</span>
#shadow-root
<style>.totes-not-global{font-weight:bold;}</style>
<span class="totes-not-global">This will be bold.</span>
<slot name="slot-0"></slot>
</div>
Attaching a shadow root requires a real DOM node. We don't want to reach up in the hierarchy and mutate the DOM, so the Root
component needs to generate a node to attach a shadow to. This defaults to a div
, but can be whatever you want. This isn't something that will ever be able to change due to the nature of the DOM and React.
There is a styled
export that is a shortcut for creating primitive components that have a default styling.
import React from "react";
import { styled } from "react-shade";
const Div = styled({
":host": {
fontSize: "1.2em",
padding: 10
}
});
A keen eye might spot some of these and notice that it's not how you'd normally do Shadow DOM with imperative JavaScript or HTML. I assure you, that is only superficial. All aspects of react-shade's API fully utilises the native APIs.
This was chosen because it's a more idiomatic way of declaring content where custom elements aren't being used. Underneath the hood, the Slot
will portal the content back to the host so it gets distributed using the built-in algorithms.
<Slot>
<span className="totes-not-global">This will NOT be bold</span>
</Slot>
This may appear to be syntactic sugar for using objects to represent your style strings, but there's a bit more to it.
<Style>
{{
selector: {
style: "property"
}
}}
</Style>
For example, this can be minified without adding anything extra to your build pipeline. You can also specify functions that react to props that are passed to Style
. Both the set of rules and each property can be specified as a function. Whatever props
that are passed to Style
will be passed through.
const rulesAsFunction = ({ font }) => ({
body: {
fontFamily: valueAsFunction
}
});
const valueAsFunction = ({ font }) => font;
<Style font={"Helvetica"}>{rulesAsFunction}</Style>;
You may also specify an Array
for a value and it will be mapped into a string using the standard rules listed above.
<Style prop={"property"}>
{{
body: {
margin: [10, 0]
}
}}
</Style>
NOTE: All numeric values will get converted to px
units.
Props are useful for passing in data from your application state. However, it's recommended you simply use CSS variables where you don't need to do that.
<Style>
{{
":root": {
"--grid-size": 5
},
body: {
margin: ["calc(var(--grid-size) * 2)", 0]
}
}}
</Style>
To use react-shade
in browsers that don't support the native APIs you'll want to include the Shadow DOM polyfill. You can find this at https://unpkg.com/@webcomponents/webcomponentsjs.
Unfortunately, that polyfill doesn't support CSS scoping. You'd normally have to find a way to use shadycss
, but integrating it is non-trivial due to its reliance on <template>
and imperative APIs.
In order to scope CSS, we've placed a dependency on shadow-css
and the CSS that you pass to <Style>
will automatically be scoped using it if you're running in a non-native environment. Check out the demo in Firefox!
Beware of the limitations of shadow-css
!