Skip to content

Web components in React and Vue: integration boundaries to test

Understand custom elements, typed wrappers, properties, custom events, slots, and server rendering when integrating a component library into React or Vue.

Velkin UI team · · Updated · 6 min read

react · vue · web-components

Sharing an interface across React and Vue is rarely just a matter of changing tag capitalization. The browser can host the same custom element in both applications, while each framework still has its own rules for properties, events, rendering, and tooling. A reliable integration makes those boundaries explicit and tests them with a small component before adopting a whole design system.

Separate the element from its framework adapter

A web component is registered with the browser and used as a custom element. A framework wrapper is an adapter around that element. The wrapper can provide familiar component exports, types, and event mappings without turning the underlying implementation into a separate React or Vue component library. This distinction matters when debugging: a problem may be in the element, adapter, bundler, or consuming application.

Velkin's MIT element package is @velkin/ui, and the documented adapters are @velkin/react and @velkin/vue. Use the feature subpaths from the installation guides rather than guessing a root import. Keep the packages on a compatible version line and retain the lockfile. A wrapper compiled against a different element contract can produce confusing symptoms even when each package individually installs.

The current React DOM documentation describes custom HTML elements and custom events. That is the primary reference for React's own behavior. A library wrapper adds its own exported contract on top. Check the specific wrapper declaration before applying advice from an older framework article or from a direct custom-element example.

Begin with an explicit React consumer

The following component is a small client-side integration. It uses feature imports, a theme provider, a real Button prop, and ordinary application state. Use the React setup for dependencies and bundler configuration. In Next.js App Router, the client boundary belongs at the interactive consumer, not automatically around every page in the application.

"use client";
import { useState } from "react";
import { VuThemeProvider } from "@velkin/react/theme-provider";
import { VuButton } from "@velkin/react/button";

export default function FrameworkBoundary() {
  const [count, setCount] = useState(0);
  return (
    <VuThemeProvider persist>
      <VuButton color="primary" onClick={() => setCount((value) => value + 1)}>
        Increment
      </VuButton>
      <p>Count: {count}</p>
    </VuThemeProvider>
  );
}

This verifies a narrow path: the wrapper import resolves, the element is registered, a supported appearance prop is applied, and a click changes React state. It does not verify server rendering of the element's internal shadow tree, form integration, or every custom event. Expand the example only after this baseline builds and behaves correctly in the target framework.

Treat properties and attributes deliberately

HTML attributes are textual markup, while JavaScript properties can carry objects, arrays, and other values. A component that consumes a complex property needs the correct assignment path. Serializing an array into an arbitrary attribute is not automatically equivalent to supplying the documented property. The wrapper and framework version determine how this boundary is handled.

Use declared types as an early check. If an editor proposes a string for a property that expects structured data, inspect the component documentation rather than forcing the assignment. For data components, identify stable record keys and keep data ownership in the application. Avoid mutating a passed object behind the framework's state model merely because the underlying element can receive it.

Boolean values deserve the same care. A textual false attribute can mean something different from a false property depending on the element's implementation. Prefer the documented wrapper usage for the framework, and inspect the rendered element when the behavior differs from expectation. Do not infer boolean semantics from how a visually similar native input works.

Read custom-event payloads from the actual API

A native click and a component-specific value event solve different problems. Velkin documents vu-prefixed custom events and wrapper mappings in its event guide. The application should read the declared event detail rather than borrowing e.target.value from a different component system. An event can bubble through a shadow boundary while its target no longer identifies the internal control you expected.

When adapting a Vue application, use its documented wrapper imports and event syntax. Do not mechanically rename every React callback into a Vue attribute. Check the current Vue installation guide and the specific component example. For two-way bindings, verify which value is controlled and which event updates it; otherwise a rerender can overwrite an apparently successful interaction.

Keep one state owner for a value. If both a framework state variable and an imperative element mutation try to control the same field, the interface may flicker or revert. A minimal reproduction should show where the value enters the component, which event reports a change, and where that event updates the source of truth. This is a stronger diagnostic than adding increasingly broad watchers.

Understand slots and styling boundaries

Named slots compose light-DOM content into a component's rendered structure. They are not necessarily equivalent to a React render prop or an arbitrary Vue template fragment. Follow the slot guide for the current wrapper's supported pattern, and inspect an actual example before moving a complex subtree between frameworks.

Shadow DOM changes where ordinary CSS selectors apply. Lit documents shadow DOM as a way to encapsulate component DOM. For application customization, prefer supported appearance props, inherited theme tokens, and documented CSS parts. An internal selector copied from browser tools is not a stable public API and can break after a package update.

Check both visual and semantic output after customization. A color override can reduce contrast, and an altered spacing rule can obscure focus. Framework independence does not remove the need to test the real consuming page. The same component can be embedded in different heading structures, forms, and navigation contexts, each with its own responsibilities.

Distinguish HTML output from complete server rendering

A page can emit custom-element tags on the server without rendering the element's internal implementation there. Hydration support and declarative shadow DOM integration are separate questions. Use the exact Next.js or Nuxt guidance for the package version you deploy. Do not describe a client-only example as a demonstration of shadow-tree SSR.

For a first Nuxt integration, isolating browser-only component setup inside an explicit client boundary can make the behavior clear. It also means that the interactive content is unavailable until the client loads. If that content matters for indexing or first paint, evaluate a supported server-rendering path instead of hiding the trade-off. Keep meaningful page text and a usable loading shell in the server output.

Test a production build, a hard refresh on the route, and navigation from another page. Development tools can mask missing imports or change timing. Watch for hydration warnings, duplicate registration, and differences between initial and subsequent rendering. Record the exact setup that passed so the next upgrade has a concrete regression target.

Adopt one boundary at a time

Use a small action, then a field, then an overlay before migrating a whole screen. Confirm types, events, focus, theming, and build output at each step. If the project needs only one framework, weigh the adapter and custom-element integration cost against the benefits of a shared cross-framework implementation. A shared core is a useful architectural option, not a universal reason to replace a working stack.

Start with the framework guides, Button reference, and MCP setup. The practical goal is a versioned, tested integration that your team understands, including its server-rendering limitations and its application-level accessibility responsibilities.

Related engineering notes

Start with Velkin UI · Connect your AI editor · Compare MIT and Pro