{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026","title":"Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks","description":"Declarative Shadow DOM, CSS parts, Lit 4.0, custom elements en todos lados, después de años de 'el año que viene es el año,' los Web Components por fin cumplieron. Qué cambió y qué sigue doliendo.","language":"es","datePublished":"2025-10-13","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks\n\n2025-10-137 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/web-components-shadow-dom-custom-elements-2026>)\n\n## Declarative Shadow DOM: el breakthrough del SSR\n\nEl bloqueador más grande para Web Components en producción era el server-side rendering. Shadow DOM era una API puramente del lado del cliente, tenías que llamar a ` attachShadow() ` en JavaScript para crear uno. Eso significaba que no había Shadow DOM hasta que JavaScript se cargara, parseara y ejecutara. Para aplicaciones SSR-heavy, era un dealbreaker.\n\nDeclarative Shadow DOM (DSD) arregla esto permitiendo que el Shadow DOM se exprese en HTML:\n\n```html\n<my-card>\n <template shadowrootmode=\"open\">\n <style>\n :host {\n display: block;\n border: 1px solid var(--card-border, #e2e0dc);\n border-radius: 8px;\n padding: 1rem;\n }\n .header { font-weight: 600; }\n .content { margin-top: 0.5rem; }\n </style>\n <div class=\"header\">\n <slot name=\"title\"></slot>\n </div>\n <div class=\"content\">\n <slot></slot>\n </div>\n </template>\n <span slot=\"title\">Card server-rendered</span>\n <p>Este shadow DOM existe antes de que corra cualquier JS.</p>\n</my-card>\n```\n\nEl browser parsea el ` <template shadowrootmode=\"open\"> ` y inmediatamente attachea un shadow root, no se necesita JavaScript. El componente renderiza con encapsulamiento de estilos completo en el first paint. Cuando tu clase de componente finalmente carga, ` connectedCallback ` se dispara y el shadow root ya existe:\n\n```javascript\nclass MyCard extends HTMLElement {\n constructor() {\n super();\n // No llamar attachShadow() si DSD ya creó uno\n if (!this.shadowRoot) {\n this.attachShadow({ mode: 'open' });\n this.shadowRoot.innerHTML = `/* template fallback */`;\n }\n }\n\n connectedCallback() {\n // El shadow root puede ya tener contenido de DSD\n this.#upgradeSlots();\n this.#attachEventListeners();\n }\n\n #upgradeSlots() {\n const titleSlot = this.shadowRoot.querySelector('slot[name=\"title\"]');\n titleSlot?.addEventListener('slotchange', () => {\n this.#updateAriaLabel();\n });\n }\n}\n\ncustomElements.define('my-card', MyCard);\n```\n\nEl patrón crítico: siempre verificar ` if (!this.shadowRoot) ` antes de llamar ` attachShadow() `. Los componentes DSD llegan con un shadow root ya poblado. Llamar ` attachShadow() ` de nuevo tira un error.\n\n### Renderizando DSD en el servidor\n\nDel lado del servidor (Node.js, Deno, o edge runtime), generar output DSD renderizando tu template como HTML:\n\n```javascript\nfunction renderMyCard({ title, content, theme = 'light' }) {\n return `\n <my-card data-theme=\"${theme}\">\n <template shadowrootmode=\"open\">\n <style>\n :host { display: block; border-radius: 8px; padding: 1rem; }\n :host([data-theme=\"dark\"]) { background: #1a1816; color: #faf9f5; }\n :host([data-theme=\"light\"]) { background: #faf9f5; color: #1a1816; }\n ::slotted([slot=\"title\"]) { font-size: 1.25rem; font-weight: 600; }\n </style>\n <slot name=\"title\"></slot>\n <slot></slot>\n </template>\n <span slot=\"title\">${escapeHtml(title)}</span>\n <div>${escapeHtml(content)}</div>\n </my-card>\n `;\n}\n```\n\nEsto funciona con cualquier framework SSR, Next.js, Astro, SvelteKit. El HTML llega completamente renderizado. Sin FOUC, sin layout shift, sin esperar JavaScript.\n\n## CSS Parts: theming bien hecho\n\nAntes de CSS parts, personalizar estilos de Web Components desde afuera era doloroso. Tenías dos opciones: CSS custom properties (limitadas a valores, no selectores) o penetrar el shadow boundary con ` ::shadow ` (deprecado y eliminado).\n\nCSS ` ::part() ` le permite a un componente exponer partes nombradas de su shadow DOM para estilos externos:\n\n```javascript\nclass DataTable extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n this.shadowRoot.innerHTML = `\n <style>\n table { width: 100%; border-collapse: collapse; }\n th { text-align: left; }\n tr:hover { background: var(--table-hover, rgba(0,0,0,0.04)); }\n </style>\n <table part=\"table\">\n <thead part=\"header\">\n <tr part=\"header-row\">\n <th part=\"header-cell\">Nombre</th>\n <th part=\"header-cell\">Valor</th>\n </tr>\n </thead>\n <tbody part=\"body\"></tbody>\n </table>\n `;\n }\n\n set data(rows) {\n const tbody = this.shadowRoot.querySelector('tbody');\n tbody.innerHTML = rows.map(row => `\n <tr part=\"row\">\n <td part=\"cell\">${row.name}</td>\n <td part=\"cell\">${row.value}</td>\n </tr>\n `).join('');\n }\n}\n```\n\nAhora los consumidores pueden estilar los internos sin tocar el código del componente:\n\n```css\ndata-table::part(header) {\n background: #d97757;\n color: white;\n}\n\ndata-table::part(header-cell) {\n padding: 0.75rem 1rem;\n font-weight: 600;\n text-transform: uppercase;\n font-size: 0.75rem;\n letter-spacing: 0.05em;\n}\n\ndata-table::part(row):hover {\n background: rgba(217, 119, 87, 0.08);\n}\n```\n\nLa belleza de las parts: el autor del componente controla qué es exponible, y el consumidor controla cómo se ve. La estructura interna del DOM puede cambiar libremente sin romper los estilos del consumidor, siempre y cuando los nombres de las parts se mantengan estables.\n\n## Custom elements asociados a formularios\n\nEste era un gap de larga data. Custom elements que wrappeaban form inputs eran invisibles al comportamiento nativo de ` <form> `, sin submission, sin validación, sin integración con ` FormData `. La API ` ElementInternals ` arregla esto completamente:\n\n```javascript\nclass StarRating extends HTMLElement {\n static formAssociated = true;\n\n #internals;\n #value = 0;\n\n constructor() {\n super();\n this.#internals = this.attachInternals();\n this.attachShadow({ mode: 'open', delegatesFocus: true });\n\n this.shadowRoot.innerHTML = `\n <style>\n :host { display: inline-flex; gap: 2px; }\n button {\n background: none; border: none; cursor: pointer;\n font-size: 1.5rem; padding: 0; color: #ccc;\n transition: color 0.15s;\n }\n button.active { color: #d97757; }\n button:focus-visible { outline: 2px solid #d97757; border-radius: 2px; }\n :host(:invalid) button.active { color: #e74c3c; }\n </style>\n ${[1,2,3,4,5].map(n => `\n <button type=\"button\" data-value=\"${n}\" aria-label=\"${n} estrella${n>1?'s':''}\">\n &#9733;\n </button>\n `).join('')}\n `;\n\n this.shadowRoot.addEventListener('click', (e) => {\n const btn = e.target.closest('button');\n if (!btn) return;\n this.value = parseInt(btn.dataset.value);\n });\n }\n\n get value() { return this.#value; }\n\n set value(v) {\n this.#value = v;\n this.#internals.setFormValue(v.toString());\n\n if (this.required && v === 0) {\n this.#internals.setValidity(\n { valueMissing: true },\n 'Por favor seleccioná una calificación',\n this.shadowRoot.querySelector('button')\n );\n } else {\n this.#internals.setValidity({});\n }\n\n this.shadowRoot.querySelectorAll('button').forEach((btn, i) => {\n btn.classList.toggle('active', i < v);\n });\n }\n\n get required() { return this.hasAttribute('required'); }\n formResetCallback() { this.value = 0; }\n formStateRestoreCallback(state) { this.value = parseInt(state) || 0; }\n}\n\ncustomElements.define('star-rating', StarRating);\n```\n\nEl componente ` star-rating ` participa en form submission, validación y reset, igual que un ` <input> ` nativo. La pseudo-clase ` :invalid ` incluso funciona a través del shadow boundary.\n\n## Lit 4.0: el framework que Web Components necesitaba\n\n[Lit](<https://lit.dev/>) 4.0 (lanzado a fines de 2025) pulió la experiencia de desarrollo al punto de sentirse competitivo con React o Vue para authoring de componentes:\n\n```javascript\nimport { LitElement, html, css } from 'lit';\nimport { property, state, query } from 'lit/decorators.js';\n\nclass UserCard extends LitElement {\n static styles = css`\n :host { display: block; container-type: inline-size; }\n .card {\n display: grid; gap: 1rem; padding: 1rem;\n border: 1px solid var(--border-color, #e2e0dc); border-radius: 8px;\n }\n @container (min-width: 400px) {\n .card { grid-template-columns: auto 1fr; }\n }\n `;\n\n @property({ type: String }) name = '';\n @property({ type: String }) role = '';\n @property({ type: String, attribute: 'avatar-url' }) avatarUrl = '';\n @state() private _expanded = false;\n\n render() {\n return html`\n <div class=\"card\" part=\"card\">\n <img src=${this.avatarUrl} alt=${this.name} part=\"avatar\" />\n <div>\n <h3 part=\"name\">${this.name}</h3>\n <p part=\"role\">${this.role}</p>\n <button @click=${this.#toggle}>\n ${this._expanded ? 'Menos' : 'Más'}\n </button>\n ${this._expanded ? html`\n <div class=\"details\" part=\"details\"><slot></slot></div>\n ` : ''}\n </div>\n </div>\n `;\n }\n\n #toggle() {\n this._expanded = !this._expanded;\n this.dispatchEvent(new CustomEvent('toggle', {\n detail: { expanded: this._expanded },\n bubbles: true, composed: true\n }));\n }\n}\n\ncustomElements.define('user-card', UserCard);\n```\n\n## La realidad de interop con frameworks\n\nReact 19+ finalmente maneja custom elements correctamente. Las propiedades pasan (no solo atributos string), los eventos funcionan, los refs funcionan. Los años de hacks con ` react-web-component-wrapper ` terminaron:\n\n```jsx\nfunction App() {\n const handleToggle = (e) => {\n console.log('Expandido:', e.detail.expanded);\n };\n\n return (\n <user-card\n name=\"Lucio Durán\"\n role=\"Software Engineer\"\n avatar-url=\"/avatar.jpg\"\n onToggle={handleToggle}\n >\n <p>Buenos Aires, Argentina</p>\n </user-card>\n );\n}\n```\n\nVue y Svelte tienen buena interop con Web Components hace años. Angular los soporta nativamente a través de ` CUSTOM_ELEMENTS_SCHEMA `. El único punto de dolor restante es la hidratación SSR, si tu framework server-renderiza HTML y un Web Component espera hidratar su output DSD, el timing puede ser tricky. El SSR de Lit maneja esto; otros frameworks pueden necesitar coordinación manual.\n\n## Cuándo realmente usarlos\n\nAl examinar design systems construidos tanto con approaches React-only como Web Components, surge una evaluación clara:\n\n**Utilizar Web Components cuando**: se tiene múltiples equipos con diferentes frameworks consumiendo la misma librería de componentes, se necesita widgets agnósticos de framework (scripts embebidos, micro-frontends), o se está construyendo para longevidad y se quiere evitar el churn de frameworks.\n\n**Continuar usando los componentes de tu framework cuando**: se es un equipo single-framework, se necesita integración profunda con features específicas del framework (React Server Components, Svelte stores), o tu equipo no quiere aprender un segundo modelo de componentes.\n\nEl punto óptimo que emerge es primitivas de design system como Web Components (botones, inputs, cards, modales) consumidas por código de aplicación específico del framework. Las primitivas cambian poco y se benefician de la independencia de framework. La lógica de aplicación cambia constantemente y se beneficia de la ergonomía del framework.\n\nNo es una toma revolucionaria, y no debería serlo. Web Components son una herramienta. Resuelven un problema específico, primitivas de UI encapsuladas, reutilizables e interoperables, y en 2026, lo resuelven bien. El ciclo de hype terminó. La fase aburrida de productividad comenzó. Eso es exactamente donde se quiere que esté una tecnología de plataforma.\n\n## Preguntas frecuentes\n\n### ¿Los Web Components están listos para producción en 2026?\n\nSí. Todos los navegadores principales soportan custom elements v1, Shadow DOM, Declarative Shadow DOM, CSS parts, y la API ElementInternals. El ecosistema (Lit 4.0, Stencil, FAST) es maduro. La fricción principal que queda es la hidratación SSR en frameworks que no son Lit.\n\n### ¿Puedo usar Web Components con React?\n\nSí, React 19+ tiene soporte completo de custom elements incluyendo pasaje de propiedades (no solo atributos), manejo de eventos, y ref forwarding. La historia de interop finalmente es sólida después de años de workarounds.\n\n### ¿Los Web Components reemplazan a React/Vue/Svelte?\n\nNo. Los Web Components son una primitiva de la plataforma para elementos de UI encapsulados y reutilizables. Los frameworks proveen state management, routing y arquitectura de aplicación. El mejor caso de uso para Web Components es componentes de design system consumidos por equipos con diferentes frameworks.\n\n## Tips\n\n1. **Tip:** Utilizar ` ::part() ` para theming, no CSS custom properties para todo. Las parts le dan a los consumidores control de estilos específico sin exponer tu estructura interna del DOM.\n2. **Tip:** Siempre establecer ` formAssociated = true ` e implementá ` ElementInternals ` si tu componente wrappea un form input. Sin esto, tu componente es invisible para el submission y validación del ` <form> `.\n3. **Tip:** Probar con Declarative Shadow DOM SSR corriendo ` getInnerHTML({includeShadowRoots: true}) ` y verificá que el output hidrate correctamente. Muchos componentes se rompen porque asumen que ` connectedCallback ` corre antes de que el shadow DOM exista.\n\n- web-components\n- shadow-dom\n- custom-elements\n- lit\n- css-parts\n- declarative-shadow-dom\n- frontend\n- interop\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#title"},{"level":2,"text":"Declarative Shadow DOM: el breakthrough del SSR","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#declarative-shadow-dom-el-breakthrough-del-ssr"},{"level":3,"text":"Renderizando DSD en el servidor","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#renderizando-dsd-en-el-servidor"},{"level":2,"text":"CSS Parts: theming bien hecho","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#css-parts-theming-bien-hecho"},{"level":2,"text":"Custom elements asociados a formularios","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#custom-elements-asociados-a-formularios"},{"level":2,"text":"Lit 4.0: el framework que Web Components necesitaba","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#lit-40-el-framework-que-web-components-necesitaba"},{"level":2,"text":"La realidad de interop con frameworks","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#la-realidad-de-interop-con-frameworks"},{"level":2,"text":"Cuándo realmente usarlos","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#cundo-realmente-usarlos"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#questions"},{"level":3,"text":"¿Los Web Components están listos para producción en 2026?","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#question-1"},{"level":3,"text":"¿Puedo usar Web Components con React?","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#question-2"},{"level":3,"text":"¿Los Web Components reemplazan a React/Vue/Svelte?","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#article-tips"}],"sections":[{"heading":"Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks","level":1,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026","content":"Frontend"},{"heading":"Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks","level":1,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#title","content":"2025-10-137 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/web-components-shadow-dom-custom-elements-2026>)"},{"heading":"Declarative Shadow DOM: el breakthrough del SSR","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#declarative-shadow-dom-el-breakthrough-del-ssr","content":"El bloqueador más grande para Web Components en producción era el server-side rendering. Shadow DOM era una API puramente del lado del cliente, tenías que llamar a ` attachShadow() ` en JavaScript para crear uno. Eso significaba que no había Shadow DOM hasta que JavaScript se cargara, parseara y ejecutara. Para aplicaciones SSR-heavy, era un dealbreaker.\n\nDeclarative Shadow DOM (DSD) arregla esto permitiendo que el Shadow DOM se exprese en HTML:\n\n```html\n<my-card>\n <template shadowrootmode=\"open\">\n <style>\n :host {\n display: block;\n border: 1px solid var(--card-border, #e2e0dc);\n border-radius: 8px;\n padding: 1rem;\n }\n .header { font-weight: 600; }\n .content { margin-top: 0.5rem; }\n </style>\n <div class=\"header\">\n <slot name=\"title\"></slot>\n </div>\n <div class=\"content\">\n <slot></slot>\n </div>\n </template>\n <span slot=\"title\">Card server-rendered</span>\n <p>Este shadow DOM existe antes de que corra cualquier JS.</p>\n</my-card>\n```\n\nEl browser parsea el ` <template shadowrootmode=\"open\"> ` y inmediatamente attachea un shadow root, no se necesita JavaScript. El componente renderiza con encapsulamiento de estilos completo en el first paint. Cuando tu clase de componente finalmente carga, ` connectedCallback ` se dispara y el shadow root ya existe:\n\n```javascript\nclass MyCard extends HTMLElement {\n constructor() {\n super();\n // No llamar attachShadow() si DSD ya creó uno\n if (!this.shadowRoot) {\n this.attachShadow({ mode: 'open' });\n this.shadowRoot.innerHTML = `/* template fallback */`;\n }\n }\n\n connectedCallback() {\n // El shadow root puede ya tener contenido de DSD\n this.#upgradeSlots();\n this.#attachEventListeners();\n }\n\n #upgradeSlots() {\n const titleSlot = this.shadowRoot.querySelector('slot[name=\"title\"]');\n titleSlot?.addEventListener('slotchange', () => {\n this.#updateAriaLabel();\n });\n }\n}\n\ncustomElements.define('my-card', MyCard);\n```\n\nEl patrón crítico: siempre verificar ` if (!this.shadowRoot) ` antes de llamar ` attachShadow() `. Los componentes DSD llegan con un shadow root ya poblado. Llamar ` attachShadow() ` de nuevo tira un error."},{"heading":"Renderizando DSD en el servidor","level":3,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#renderizando-dsd-en-el-servidor","content":"Del lado del servidor (Node.js, Deno, o edge runtime), generar output DSD renderizando tu template como HTML:\n\n```javascript\nfunction renderMyCard({ title, content, theme = 'light' }) {\n return `\n <my-card data-theme=\"${theme}\">\n <template shadowrootmode=\"open\">\n <style>\n :host { display: block; border-radius: 8px; padding: 1rem; }\n :host([data-theme=\"dark\"]) { background: #1a1816; color: #faf9f5; }\n :host([data-theme=\"light\"]) { background: #faf9f5; color: #1a1816; }\n ::slotted([slot=\"title\"]) { font-size: 1.25rem; font-weight: 600; }\n </style>\n <slot name=\"title\"></slot>\n <slot></slot>\n </template>\n <span slot=\"title\">${escapeHtml(title)}</span>\n <div>${escapeHtml(content)}</div>\n </my-card>\n `;\n}\n```\n\nEsto funciona con cualquier framework SSR, Next.js, Astro, SvelteKit. El HTML llega completamente renderizado. Sin FOUC, sin layout shift, sin esperar JavaScript."},{"heading":"CSS Parts: theming bien hecho","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#css-parts-theming-bien-hecho","content":"Antes de CSS parts, personalizar estilos de Web Components desde afuera era doloroso. Tenías dos opciones: CSS custom properties (limitadas a valores, no selectores) o penetrar el shadow boundary con ` ::shadow ` (deprecado y eliminado).\n\nCSS ` ::part() ` le permite a un componente exponer partes nombradas de su shadow DOM para estilos externos:\n\n```javascript\nclass DataTable extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n this.shadowRoot.innerHTML = `\n <style>\n table { width: 100%; border-collapse: collapse; }\n th { text-align: left; }\n tr:hover { background: var(--table-hover, rgba(0,0,0,0.04)); }\n </style>\n <table part=\"table\">\n <thead part=\"header\">\n <tr part=\"header-row\">\n <th part=\"header-cell\">Nombre</th>\n <th part=\"header-cell\">Valor</th>\n </tr>\n </thead>\n <tbody part=\"body\"></tbody>\n </table>\n `;\n }\n\n set data(rows) {\n const tbody = this.shadowRoot.querySelector('tbody');\n tbody.innerHTML = rows.map(row => `\n <tr part=\"row\">\n <td part=\"cell\">${row.name}</td>\n <td part=\"cell\">${row.value}</td>\n </tr>\n `).join('');\n }\n}\n```\n\nAhora los consumidores pueden estilar los internos sin tocar el código del componente:\n\n```css\ndata-table::part(header) {\n background: #d97757;\n color: white;\n}\n\ndata-table::part(header-cell) {\n padding: 0.75rem 1rem;\n font-weight: 600;\n text-transform: uppercase;\n font-size: 0.75rem;\n letter-spacing: 0.05em;\n}\n\ndata-table::part(row):hover {\n background: rgba(217, 119, 87, 0.08);\n}\n```\n\nLa belleza de las parts: el autor del componente controla qué es exponible, y el consumidor controla cómo se ve. La estructura interna del DOM puede cambiar libremente sin romper los estilos del consumidor, siempre y cuando los nombres de las parts se mantengan estables."},{"heading":"Custom elements asociados a formularios","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#custom-elements-asociados-a-formularios","content":"Este era un gap de larga data. Custom elements que wrappeaban form inputs eran invisibles al comportamiento nativo de ` <form> `, sin submission, sin validación, sin integración con ` FormData `. La API ` ElementInternals ` arregla esto completamente:\n\n```javascript\nclass StarRating extends HTMLElement {\n static formAssociated = true;\n\n #internals;\n #value = 0;\n\n constructor() {\n super();\n this.#internals = this.attachInternals();\n this.attachShadow({ mode: 'open', delegatesFocus: true });\n\n this.shadowRoot.innerHTML = `\n <style>\n :host { display: inline-flex; gap: 2px; }\n button {\n background: none; border: none; cursor: pointer;\n font-size: 1.5rem; padding: 0; color: #ccc;\n transition: color 0.15s;\n }\n button.active { color: #d97757; }\n button:focus-visible { outline: 2px solid #d97757; border-radius: 2px; }\n :host(:invalid) button.active { color: #e74c3c; }\n </style>\n ${[1,2,3,4,5].map(n => `\n <button type=\"button\" data-value=\"${n}\" aria-label=\"${n} estrella${n>1?'s':''}\">\n &#9733;\n </button>\n `).join('')}\n `;\n\n this.shadowRoot.addEventListener('click', (e) => {\n const btn = e.target.closest('button');\n if (!btn) return;\n this.value = parseInt(btn.dataset.value);\n });\n }\n\n get value() { return this.#value; }\n\n set value(v) {\n this.#value = v;\n this.#internals.setFormValue(v.toString());\n\n if (this.required && v === 0) {\n this.#internals.setValidity(\n { valueMissing: true },\n 'Por favor seleccioná una calificación',\n this.shadowRoot.querySelector('button')\n );\n } else {\n this.#internals.setValidity({});\n }\n\n this.shadowRoot.querySelectorAll('button').forEach((btn, i) => {\n btn.classList.toggle('active', i < v);\n });\n }\n\n get required() { return this.hasAttribute('required'); }\n formResetCallback() { this.value = 0; }\n formStateRestoreCallback(state) { this.value = parseInt(state) || 0; }\n}\n\ncustomElements.define('star-rating', StarRating);\n```\n\nEl componente ` star-rating ` participa en form submission, validación y reset, igual que un ` <input> ` nativo. La pseudo-clase ` :invalid ` incluso funciona a través del shadow boundary."},{"heading":"Lit 4.0: el framework que Web Components necesitaba","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#lit-40-el-framework-que-web-components-necesitaba","content":"[Lit](<https://lit.dev/>) 4.0 (lanzado a fines de 2025) pulió la experiencia de desarrollo al punto de sentirse competitivo con React o Vue para authoring de componentes:\n\n```javascript\nimport { LitElement, html, css } from 'lit';\nimport { property, state, query } from 'lit/decorators.js';\n\nclass UserCard extends LitElement {\n static styles = css`\n :host { display: block; container-type: inline-size; }\n .card {\n display: grid; gap: 1rem; padding: 1rem;\n border: 1px solid var(--border-color, #e2e0dc); border-radius: 8px;\n }\n @container (min-width: 400px) {\n .card { grid-template-columns: auto 1fr; }\n }\n `;\n\n @property({ type: String }) name = '';\n @property({ type: String }) role = '';\n @property({ type: String, attribute: 'avatar-url' }) avatarUrl = '';\n @state() private _expanded = false;\n\n render() {\n return html`\n <div class=\"card\" part=\"card\">\n <img src=${this.avatarUrl} alt=${this.name} part=\"avatar\" />\n <div>\n <h3 part=\"name\">${this.name}</h3>\n <p part=\"role\">${this.role}</p>\n <button @click=${this.#toggle}>\n ${this._expanded ? 'Menos' : 'Más'}\n </button>\n ${this._expanded ? html`\n <div class=\"details\" part=\"details\"><slot></slot></div>\n ` : ''}\n </div>\n </div>\n `;\n }\n\n #toggle() {\n this._expanded = !this._expanded;\n this.dispatchEvent(new CustomEvent('toggle', {\n detail: { expanded: this._expanded },\n bubbles: true, composed: true\n }));\n }\n}\n\ncustomElements.define('user-card', UserCard);\n```"},{"heading":"La realidad de interop con frameworks","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#la-realidad-de-interop-con-frameworks","content":"React 19+ finalmente maneja custom elements correctamente. Las propiedades pasan (no solo atributos string), los eventos funcionan, los refs funcionan. Los años de hacks con ` react-web-component-wrapper ` terminaron:\n\n```jsx\nfunction App() {\n const handleToggle = (e) => {\n console.log('Expandido:', e.detail.expanded);\n };\n\n return (\n <user-card\n name=\"Lucio Durán\"\n role=\"Software Engineer\"\n avatar-url=\"/avatar.jpg\"\n onToggle={handleToggle}\n >\n <p>Buenos Aires, Argentina</p>\n </user-card>\n );\n}\n```\n\nVue y Svelte tienen buena interop con Web Components hace años. Angular los soporta nativamente a través de ` CUSTOM_ELEMENTS_SCHEMA `. El único punto de dolor restante es la hidratación SSR, si tu framework server-renderiza HTML y un Web Component espera hidratar su output DSD, el timing puede ser tricky. El SSR de Lit maneja esto; otros frameworks pueden necesitar coordinación manual."},{"heading":"Cuándo realmente usarlos","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#cundo-realmente-usarlos","content":"Al examinar design systems construidos tanto con approaches React-only como Web Components, surge una evaluación clara:\n\n**Utilizar Web Components cuando**: se tiene múltiples equipos con diferentes frameworks consumiendo la misma librería de componentes, se necesita widgets agnósticos de framework (scripts embebidos, micro-frontends), o se está construyendo para longevidad y se quiere evitar el churn de frameworks.\n\n**Continuar usando los componentes de tu framework cuando**: se es un equipo single-framework, se necesita integración profunda con features específicas del framework (React Server Components, Svelte stores), o tu equipo no quiere aprender un segundo modelo de componentes.\n\nEl punto óptimo que emerge es primitivas de design system como Web Components (botones, inputs, cards, modales) consumidas por código de aplicación específico del framework. Las primitivas cambian poco y se benefician de la independencia de framework. La lógica de aplicación cambia constantemente y se beneficia de la ergonomía del framework.\n\nNo es una toma revolucionaria, y no debería serlo. Web Components son una herramienta. Resuelven un problema específico, primitivas de UI encapsuladas, reutilizables e interoperables, y en 2026, lo resuelven bien. El ciclo de hype terminó. La fase aburrida de productividad comenzó. Eso es exactamente donde se quiere que esté una tecnología de plataforma."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#questions","content":""},{"heading":"¿Los Web Components están listos para producción en 2026?","level":3,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#question-1","content":"Sí. Todos los navegadores principales soportan custom elements v1, Shadow DOM, Declarative Shadow DOM, CSS parts, y la API ElementInternals. El ecosistema (Lit 4.0, Stencil, FAST) es maduro. La fricción principal que queda es la hidratación SSR en frameworks que no son Lit."},{"heading":"¿Puedo usar Web Components con React?","level":3,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#question-2","content":"Sí, React 19+ tiene soporte completo de custom elements incluyendo pasaje de propiedades (no solo atributos), manejo de eventos, y ref forwarding. La historia de interop finalmente es sólida después de años de workarounds."},{"heading":"¿Los Web Components reemplazan a React/Vue/Svelte?","level":3,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#question-3","content":"No. Los Web Components son una primitiva de la plataforma para elementos de UI encapsulados y reutilizables. Los frameworks proveen state management, routing y arquitectura de aplicación. El mejor caso de uso para Web Components es componentes de design system consumidos por equipos con diferentes frameworks."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#article-tips","content":"1. **Tip:** Utilizar ` ::part() ` para theming, no CSS custom properties para todo. Las parts le dan a los consumidores control de estilos específico sin exponer tu estructura interna del DOM.\n2. **Tip:** Siempre establecer ` formAssociated = true ` e implementá ` ElementInternals ` si tu componente wrappea un form input. Sin esto, tu componente es invisible para el submission y validación del ` <form> `.\n3. **Tip:** Probar con Declarative Shadow DOM SSR corriendo ` getInnerHTML({includeShadowRoots: true}) ` y verificá que el output hidrate correctamente. Muchos componentes se rompen porque asumen que ` connectedCallback ` corre antes de que el shadow DOM exista.\n\n- web-components\n- shadow-dom\n- custom-elements\n- lit\n- css-parts\n- declarative-shadow-dom\n- frontend\n- interop\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/web-components-shadow-dom-custom-elements-2026"},{"text":"Lit","url":"https://lit.dev/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026"},"headline":"Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks","description":"Declarative Shadow DOM, CSS parts, Lit 4.0, custom elements en todos lados, después de años de 'el año que viene es el año,' los Web Components por fin cumplieron. Qué cambió y qué sigue doliendo.","datePublished":"2025-10-13","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"web-components, shadow-dom, custom-elements, lit, css-parts, declarative-shadow-dom, frontend, interop","wordCount":1324,"timeRequired":"PT7M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/web-components-shadow-dom-custom-elements-2026"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Web Components en 2026: Declarative Shadow DOM, CSS Parts e Interoperabilidad con Frameworks","item":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Los Web Components están listos para producción en 2026?","acceptedAnswer":{"@type":"Answer","text":"Sí. Todos los navegadores principales soportan custom elements v1, Shadow DOM, Declarative Shadow DOM, CSS parts, y la API ElementInternals. El ecosistema (Lit 4.0, Stencil, FAST) es maduro. La fricción principal que queda es la hidratación SSR en frameworks que no son Lit."}},{"@type":"Question","name":"¿Puedo usar Web Components con React?","acceptedAnswer":{"@type":"Answer","text":"Sí, React 19+ tiene soporte completo de custom elements incluyendo pasaje de propiedades (no solo atributos), manejo de eventos, y ref forwarding. La historia de interop finalmente es sólida después de años de workarounds."}},{"@type":"Question","name":"¿Los Web Components reemplazan a React/Vue/Svelte?","acceptedAnswer":{"@type":"Answer","text":"No. Los Web Components son una primitiva de la plataforma para elementos de UI encapsulados y reutilizables. Los frameworks proveen state management, routing y arquitectura de aplicación. El mejor caso de uso para Web Components es componentes de design system consumidos por equipos con diferentes frameworks."}}]}]}],"contentHash":"ba46c6479a8d54d730ac7c8bbfe5e980cbc0d0eab48339e3c7d73c5c033740fe","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/web-components-shadow-dom-custom-elements-2026","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":64553,"contentBytes":13003}}
