diff --git a/packages/react-docs/config/sidebar-routes.js b/packages/react-docs/config/sidebar-routes.js
index 7532419034..e2fdd07494 100644
--- a/packages/react-docs/config/sidebar-routes.js
+++ b/packages/react-docs/config/sidebar-routes.js
@@ -10,6 +10,7 @@ import {
MigrateSuccessIcon,
RocketIcon,
SVGIcon,
+ ToolsConfigurationIcon,
UserTeamIcon,
WidgetsIcon,
WorkspaceIcon,
@@ -61,6 +62,15 @@ export const routes = [
{ title: 'React Icons', path: 'contributing/react-icons' },
],
},
+ {
+ title: 'Customization',
+ icon: (props) => (
+
+ ),
+ routes: [
+ { title: 'Shadow DOM', path: 'customization/shadow-dom' },
+ ],
+ },
{
title: 'Migrations',
icon: (props) => (
diff --git a/packages/react-docs/pages/customization/shadow-dom.js b/packages/react-docs/pages/customization/shadow-dom.js
new file mode 100644
index 0000000000..e374e0e594
--- /dev/null
+++ b/packages/react-docs/pages/customization/shadow-dom.js
@@ -0,0 +1,402 @@
+import createCache from '@emotion/cache';
+import { CacheProvider } from '@emotion/react';
+import {
+ Box,
+ Button,
+ Divider,
+ Drawer,
+ DrawerOverlay,
+ DrawerContent,
+ DrawerHeader,
+ DrawerBody,
+ DrawerFooter,
+ Flex,
+ Grid,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ PortalManager,
+ Skeleton,
+ Stack,
+ Text,
+ Toast,
+ ToastManager,
+ TonicProvider,
+ Tooltip,
+ createTheme,
+ useColorMode,
+ usePortalManager,
+ useToastManager,
+} from '@tonic-ui/react';
+import BorderedBox from '@/components/BorderedBox';
+import React, { useEffect, useRef } from 'react';
+import { createRoot } from 'react-dom/client';
+
+const NONCE = process.env.NONCE ?? '';
+
+const DrawerComponent = ({ onClose }) => {
+ return (
+
+
+
+
+ Drawer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ModalComponent = ({ onClose }) => {
+ return (
+
+
+
+
+ Modal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ShadowDOMContainer = ({ children, colorMode, ...rest }) => {
+ const containerRef = useRef();
+ const shadowRootElementRef = useRef();
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+
+ const shadowContainer = container.shadowRoot ?? container.attachShadow({ mode: 'open' });
+ const shadowRootElement = document.createElement('div');
+ shadowContainer.appendChild(shadowRootElement);
+ shadowRootElementRef.current = shadowRootElement;
+
+ return () => {
+ // Empty the shadow DOM content
+ if (shadowContainer) {
+ shadowContainer.innerHTML = '';
+ }
+
+ shadowRootElementRef.current = null;
+ };
+ }, []); // Run only once on mount
+
+ useEffect(() => {
+ const shadowRootElement = shadowRootElementRef.current;
+ const shadowContainer = shadowRootElement.parentNode;
+ const root = createRoot(shadowRootElement);
+ const cache = createCache({
+ key: 'tonic-ui-shadow',
+ nonce: NONCE, // Needed to comply with Content Security Policy (CSP) for inline execution
+ prepend: true,
+ container: shadowContainer,
+ });
+ const shadowTheme = createTheme({
+ cssVariables: {
+ prefix: 'tonic-shadow',
+ rootSelector: ':host',
+ },
+ components: {
+ Drawer: {
+ defaultProps: {
+ portalProps: {
+ containerRef: shadowRootElementRef,
+ },
+ },
+ },
+ Modal: {
+ defaultProps: {
+ portalProps: {
+ containerRef: shadowRootElementRef,
+ },
+ },
+ },
+ Popper: {
+ defaultProps: {
+ portalProps: {
+ containerRef: shadowRootElementRef,
+ },
+ },
+ },
+ },
+ });
+
+ root.render(
+
+
+ &:first-of-type': {
+ mt: '4x', // the space to the top edge of the screen
+ },
+ '[data-toast-placement^="bottom"] > &:last-of-type': {
+ mb: '4x', // the space to the bottom edge of the screen
+ },
+ '[data-toast-placement$="left"] > &': {
+ ml: '4x', // the space to the left edge of the screen
+ },
+ '[data-toast-placement$="right"] > &': {
+ mr: '4x', // the space to the right edge of the screen
+ },
+ },
+ }}
+ containerRef={shadowRootElementRef}
+ >
+
+ {children}
+
+
+
+
+ );
+ }, [children, colorMode]);
+
+ return (
+
+ );
+};
+
+const InsideShadowDOMComponent = () => {
+ const portal = usePortalManager();
+ const toast = useToastManager();
+ const handleClickDrawer = () => {
+ portal((close) => );
+ };
+ const handleClickModal = () => {
+ portal((close) => );
+ };
+ const handleClickToast = () => {
+ const render = ({ id, onClose, placement }) => {
+ const isTop = placement.includes('top');
+ const toastSpacingKey = isTop ? 'pb' : 'pt';
+ const styleProps = {
+ [toastSpacingKey]: '2x',
+ width: 320,
+ };
+ return (
+
+
+ This is a toast notification
+
+
+ );
+ };
+ const options = {
+ duration: 5000,
+ };
+ toast(render, options);
+ };
+
+ return (
+
+
+ Inside Shadow DOM
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const OutsideShadowDOMComponent = () => {
+ const portal = usePortalManager();
+ const toast = useToastManager();
+ const handleClickDrawer = () => {
+ portal((close) => );
+ };
+ const handleClickModal = () => {
+ portal((close) => );
+ };
+ const handleClickToast = () => {
+ const render = ({ id, onClose, placement }) => {
+ const isTop = placement.includes('top');
+ const toastSpacingKey = isTop ? 'pb' : 'pt';
+ const styleProps = {
+ [toastSpacingKey]: '2x',
+ width: 320,
+ };
+ return (
+
+
+ This is a toast notification
+
+
+ );
+ };
+ const options = {
+ duration: 5000,
+ };
+ toast(render, options);
+ };
+
+ return (
+
+
+ Outside Shadow DOM
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const App = () => {
+ const [colorMode] = useColorMode();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/packages/react-docs/pages/customization/shadow-dom.page.mdx b/packages/react-docs/pages/customization/shadow-dom.page.mdx
new file mode 100644
index 0000000000..d75793a76e
--- /dev/null
+++ b/packages/react-docs/pages/customization/shadow-dom.page.mdx
@@ -0,0 +1,140 @@
+# Shadow DOM
+
+The shadow DOM allows you to encapsulate parts of your application, isolating them from global styles and preventing interference with the regular DOM tree.
+
+## Shadow DOM Integration
+
+To enable styling within the shadow DOM, start by importing `createCache` and `CacheProvider`:
+
+```js
+import createCache from '@emotion/cache';
+import { CacheProvider } from '@emotion/react';
+```
+
+### 1. Applying styles inside the shadow DOM
+
+The shadow DOM creates an isolated DOM tree attached to a host element, helping prevent style conflicts across components. Below is an example of how to create and style a shadow DOM:
+
+```js
+const container = document.querySelector('#root');
+const shadowContainer = container.shadowRoot || container.attachShadow({ mode: 'open' });
+const shadowRootElement = document.createElement('div');
+shadowContainer.appendChild(shadowRootElement);
+
+// Consider using `const shadowRootElementRef = useRef();` if working within a functional component
+const shadowRootElementRef = {
+ current: shadowRootElement,
+};
+
+const cache = createCache({
+ key: 'tonic-ui-shadow', // or 'css'
+ prepend: true,
+ container: shadowContainer,
+ nonce, // [optional] to comply with Content Security Policy (CSP) for inline execution
+});
+```
+
+### 2. Theming components inside the shadow DOM
+
+Components such as `Drawer`, `Modal`, and `Popper` from Tonic UI often render outside the main DOM hierarchy using a `Portal`. By default, these portals render in `document.body`. When using the shadow DOM, they need to render within the shadow container:
+
+```js
+const shadowTheme = createTheme({
+ components: {
+ Drawer: {
+ defaultProps: {
+ portalProps: {
+ containerRef: shadowRootElementRef,
+ },
+ },
+ },
+ Modal: {
+ defaultProps: {
+ portalProps: {
+ containerRef: shadowRootElementRef,
+ },
+ },
+ },
+ Popper: {
+ defaultProps: {
+ portalProps: {
+ containerRef: shadowRootElementRef,
+ },
+ },
+ },
+ },
+});
+```
+
+### 3. Using CSS theme variables (optional)
+
+To use CSS theme variables within the shadow DOM, specify the root selector for generating the CSS variables:
+
+```js
+const shadowTheme = createTheme({
+ cssVariables: {
+ prefix: 'tonic-shadow',
+ rootSelector: ':host',
+ },
+ components: {
+ // Same as the above step
+ },
+});
+```
+
+### 4. Rendering components inside the shadow DOM
+
+The following code snippet illustrates how to render a React application inside the shadow DOM:
+
+```jsx
+React.createRoot(shadowRootElement).render(
+
+
+ &:first-of-type': {
+ mt: '4x', // the space to the top edge of the screen
+ },
+ '[data-toast-placement^="bottom"] > &:last-of-type': {
+ mb: '4x', // the space to the bottom edge of the screen
+ },
+ '[data-toast-placement$="left"] > &': {
+ ml: '4x', // the space to the left edge of the screen
+ },
+ '[data-toast-placement$="right"] > &': {
+ mr: '4x', // the space to the right edge of the screen
+ },
+ },
+ }}
+ containerRef={shadowRootElementRef}
+ >
+
+
+
+
+
+
+);
+```
+
+## Demo
+
+This example applies a global button style. The button outside the shadow DOM inherits the global styling, while the button inside the shadow DOM remains unaffected:
+
+```jsx
+sx={{
+ 'button': {
+ opacity: '.65 !important',
+ },
+}}
+```
+
+{render('./shadow-dom')}