Алексей Назаренко, MinskCSS #12, 2024.
<html lang="ru">
	<head>
		<title>Лучшие кроссовки</title>
	</head>
	<body>
		<form action="/search" method="get" autocomplete="off">
			<label for="search">Поиск:</label>
			<input type="search" id="search" name="q" placeholder="Кроссовки">
			<button>Найти<button>
		</form>
	</body>
</html>
<input type="search" id="search" name="q" placeholder="Кроссовки">
	#shadow-root (user-agent)
		<div pseudo="-webkit-input-placeholder" id="placeholder" style="display: block !important;">Кроссовки</div>
		<div id="text-field-container" pseudo="-webkit-textfield-decoration-container" style="unicode-bidi: normal;">
			<div id="editing-view-port">
				<div></div>
			</div>
			<div pseudo="-webkit-search-cancel-button" id="search-clear" style="opacity: 0; pointer-events: none;"></div>
		</div>
</input>
Button
<sl-button variant="primary">
	#shadow-root (open)
		<button part="base" class="button button--primary button--medium button--standard button--has-label" type="button" title name value role="button" aria-disabled="false" tabindex="0">
			<slot name="prefix" part="prefix" class="button__prefix"></slot>
			<slot part="label" class="button__label">Button</slot>
			<slot name="suffix" part="suffix" class="button__suffix"></slot>
		</button>
	Button
</sl-button>
<sl-button variant="primary"> <!-- Shadow Host -->
<!-- Shadow Boundary -->
	#shadow-root (open) <!-- Shadow Root -->
		<!-- Shadow Tree -->
		<button part="base" class="button button--primary button--medium button--standard button--has-label" type="button" title name value role="button" aria-disabled="false" tabindex="0">
			<slot name="prefix" part="prefix" class="button__prefix"></slot>
			<slot part="label" class="button__label">Button</slot>
			<slot name="suffix" part="suffix" class="button__suffix"></slot>
		</button>
<!-- Shadow Boundary -->
	Button
</sl-button>
Shadow DOM позволяет добавить к элементу поддерево DOM с изолированной* разметкой, стилями и поведением.
* Есть нюансы.
<div class="host"></div>
const host = document.querySelector('.host');
host.attachShadow({ mode: 'open' });
const h1 = document.createElement('h1');
h1.textContent = 'Hello from shadow!';
host.shadowRoot.append(h1);
<div class="host">
	<template shadowrootmode="open">
		<h1>Hello from shadow!</h1>
	</template>
</div>
(function attachShadowRoots(root) {
	root.querySelectorAll("template[shadowrootmode]").forEach(template => {
		const mode = template.getAttribute("shadowrootmode");
		const shadowRoot = template.parentNode.attachShadow({ mode });
		shadowRoot.appendChild(template.content);
		template.remove();
		attachShadowRoots(shadowRoot);
	});
})(document);
<article><aside><blockquote><body><div><footer><h*><header><main><nav><p><section><span><form action="/subscribe" method="post">
	<label for="email">Email:</label>
	<div class="field">
		#shadow-root (open)
			<input type="email" id="email" aria-describedby="error" required>
			<svg id="error"><!-- ... --></svg>
	</div>
	<p id="error">Не заполнено поле email</p>
	<button>Подписаться</button>
</form>
<form action="/subscribe" method="post"> <!-- Данные из поля не отправятся -->
	<label for="email">Email:</label>
	<div class="field">
		#shadow-root (open)
			<input type="email" id="email" aria-describedby="error" required>
			<svg id="error"><!-- ... --></svg>
	</div>
	<p id="error">Не заполнено поле email</p>
	<button>Подписаться</button>
</form>
<form action="/subscribe" method="post">
	<label for="email">Email:</label>
	<div class="field">
		#shadow-root (open)
			<input type="email" id="email" aria-describedby="error" required> <!-- Не валидируется -->
			<svg id="error"><!-- ... --></svg>
	</div>
	<p id="error">Не заполнено поле email</p>
	<button>Подписаться</button>
</form>
<form action="/subscribe" method="post">
	<label for="email">Email:</label> <!-- Лейбл не привяжется -->
	<div class="field">
		#shadow-root (open)
			<input type="email" id="email" aria-describedby="error" required>
			<svg id="error"><!-- ... --></svg>
	</div>
	<p id="error">Не заполнено поле email</p> <!-- Ошибка не привяжется -->
	<button>Подписаться</button>
</form>
<form action="/subscribe" method="post">
	<label for="email">Email:</label>
	<div class="field">
		#shadow-root (open)
			<input type="email" id="email" aria-describedby="error" required>
			<svg id="error"><!-- ... --></svg> <!-- id может повторяться -->
	</div>
	<p id="error">Не заполнено поле email</p> <!-- id может повторяться -->
	<button>Подписаться</button>
</form>
<script>
	const foo = 'bar';
</script>
<div class="field">
	#shadow-root (open)
		<script>
			console.log(foo); // 'bar'
			console.log(window); // window
		</script>
		<input type="email" id="email" required>
		<svg id="error"><!-- ... --></svg>
</div>
<script>
	console.log(document.querySelector('input')); // null
</script>
<div class="field">
	#shadow-root (open)
		<script>
			console.log(document.querySelector('.field')); // null
		</script>
		<input type="email" id="email" required>
		<svg id="error"><!-- ... --></svg>
</div>
mode: 'open'<script>
	const field = document.querySelector('.field');
	console.log(field.shadowRoot); // #shadow-root (open)
	console.log(field.shadowRoot.firstElementChild); // <input>
</script>
<div class="field">
	#shadow-root (open)
		<input type="email" id="email" required>
		<svg id="error"><!-- ... --></svg>
</div>
mode: 'closed'<script>
	const field = document.querySelector('.field');
	console.log(field.shadowRoot); // null
	console.log(field.shadowRoot.firstElementChild); // TypeError
</script>
<div class="field">
	#shadow-root (closed)
		<input type="email" id="email" required>
		<svg id="error"><!-- ... --></svg>
</div>
<div class="host"></div>
const host = document.querySelector('.host');
host.attachShadow({ mode: 'open' });
host.shadowRoot.innerHTML = '<p>Shadow DOM</p>';
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync('p { color: red; }'); // CSS-in-JS!
host.shadowRoot.adoptedStyleSheets.push(stylesheet);
<div class="host">
	<template shadowrootmode="open">
		<style>
			p {
				color: red;
			}
		</style>
		<style>@import url('/styles.css');</style>
		<link rel="stylesheet" href="/styles.css">
		<p>Shadow DOM</p>
	</template>
</div>
<style>
	p {
		color: red;
	}
</style>
<p>Light DOM</p>
<div>
	#shadow-root (open)
		<p>Shadow DOM</p>
</div>
Light DOM
Shadow DOM
<p>Light DOM</p>
<div>
	#shadow-root (open)
		<style>
			p {
				color: red;
			}
		</style>
		<p>Shadow DOM</p>
</div>
Light DOM
Shadow DOM
<style>
	.parent {
		color: red;
	}
</style>
<div class="parent">
	<p>Light DOM</p>
	<div class="host">
		#shadow-root (open)
			<p>Shadow DOM</p>
	</div>
</div>
Light DOM
Shadow DOM
azimuthborder-collapseborder-spacingcaption-sidecolorcursordirectionempty-cellsfont-*letter-spacingline-heightlist-style-*orphansquotestext-aligntext-indenttext-transformvisibilitywhite-spacewidowsword-spacing<div class="host">
	#shadow-root (open)
		<style>
			:host {
				border: 2px dashed red;
			}
		</style>
		<p>Shadow DOM</p>
</div>
Shadow DOM
<style>
	.host {
		border: 2px dotted green;
	}
</style>
<div class="host">
	#shadow-root (open)
		<style>
			:host {
				border: 2px dashed red;
			}
		</style>
		<p>Shadow DOM</p>
</div>
Shadow DOM
The CSS Cascade: A Deep Dive
<div class="host green">
	#shadow-root (open)
		<style>
			:host-context(.green) {
				border: 2px dashed green;
			}
		</style>
		<p>Shadow DOM</p>
</div>
Shadow DOM
<style>
	:root {
		--color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<style>
			p {
				color: var(--color);
			}
		</style>
		<p>Shadow DOM</p>
</div>
Shadow DOM
:host, :host() и :host-context().<style>
	.host::part(p) {
		border: 2px dashed green;
	}
	.host::part(p)::first-letter {
		color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<p part="p">Shadow DOM</p>
</div>
Shadow DOM
<style>
	.host::part(p) span { /* Так нельзя */
		color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<p part="p">
			Shadow <span>DOM<span>
		</p>
</div>
Shadow DOM
<style>
	.host::part(s) { /* А так можно */
		color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<p part="p">
			Shadow <span part="s">DOM<span>
		</p>
</div>
Shadow DOM
<style>
	.host::part(s) { /* Не работает */
		color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<p>
			#shadow-root (open)
				Shadow
				<span part="s">DOM<span>
		</p>
</div>
Shadow DOM
<style>
	.host::part(s) { /* Теперь работает */
		color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<p exportparts="s"> <!-- экспорт -->
			#shadow-root (open)
				Shadow
				<span part="s">DOM<span>
		</p>
</div>
Shadow DOM
<style>
	.host::part(span) { /* Другое имя */
		color: red;
	}
</style>
<div class="host">
	#shadow-root (open)
		<p exportparts="s:span">
			#shadow-root (open)
				Shadow
				<span part="s">DOM<span>
		</p>
</div>
Shadow DOM
part можно стилизовать снаружи при помощи ::part().part="foo bar".exportparts.exportparts="foo,bar") и переименовывать их (exportparts="foo:baz").<div class="host">
	<template shadowrootmode="open">
		<p>Shadow DOM</p>
	</template>
	<p>Light DOM</p>
</div>
Shadow DOM
Light DOM
<div class="host">
	<template shadowrootmode="open">
		<p>Shadow DOM</p>
		<slot></slot>
	</template>
	<p>Light DOM</p>
</div>
Shadow DOM
Light DOM
<div class="host">
	<template shadowrootmode="open">
		<slot name="before"></slot>
		<p>Shadow DOM</p>
		<slot></slot>
	</template>
	<p>Light DOM default</p>
	<p slot="before">Light DOM before</p>
</div>
Shadow DOM
Light DOM default
Light DOM before
<style>
	p {
		color: red;
	}
</style>
<div class="host">
	<template shadowrootmode="open">
		<p>Shadow DOM</p>
		<slot></slot>
	</template>
	<p>Light DOM</p>
</div>
Shadow DOM
<div class="host">
	<template shadowrootmode="open">
		<style>
			::slotted(*) {
				color: red;
			}
		</style>
		<p>Shadow DOM</p>
		<slot></slot>
	</template>
	<p>Light DOM</p>
</div>
Shadow DOM
Light DOM
<div class="host">
	<template shadowrootmode="open">
		<style>
			::slotted(div) {
				color: red;
			}
		</style>
		<p>Shadow DOM</p>
		<slot></slot>
	</template>
	<div>Light DOM div</div>
	<p>Light DOM p</p>
</div>
Shadow DOM
Light DOM p
<style>
	p { color: red; }
</style>
<div class="host">
	<template shadowrootmode="open">
		<style>
			::slotted(p) { color: green; }
		</style>
		<slot></slot>
	</template>
	<p>Light DOM span</p>
</div>
Light DOM span
::slotted().::slotted() из-за каскада.@scope в CSS
Слайды: shadow-dom-minskcss.netlify.app
Канал: <divelopers>
Telegram: @alexnozer
Алексей Назаренко