Ship One Stencil Component to React, Angular, Vue & Vanilla with a Single Build
Stencil’s output targets let you compile a single component source into framework‑specific wrappers and a standard Custom Element bundle. This guide shows how to configure, build, and consume the component in React, Angular, Vue, and plain HTML, including a practical example and key trade‑offs.
02 Sept 2025, 03:16 UTC

Why One Build for Multiple Frameworks?
Design systems that target React, Angular, Vue and vanilla apps usually ship a different library for each ecosystem. That duplicates code, increases bundle size, and complicates maintenance. StencilJS solves this by compiling the same component source into a standards‑based Custom Element and, at the same time, generating lightweight wrappers that fit the host framework’s API. The result is a single source of truth that can be consumed anywhere without a runtime bridge.
Configuring Output Targets
Stencil’s stencil.config.ts controls what gets emitted. The outputTargets array can include:
dist– the standard Custom Element bundle.dist-custom-elements-bundle– a single file for<script>inclusion.react,angular,vue– framework wrappers.docs-readme– auto‑generated documentation.
Below is a minimal configuration that emits all four framework targets:
import { defineConfig } from '@stencil/core';
export default defineConfig({
namespace: 'my-lib',
outputTargets: [
{ type: 'dist' },
{ type: 'dist-custom-elements-bundle' },
{ type: 'react' },
{ type: 'angular' },
{ type: 'vue' }
]
});
Run npm run build (or yarn build) to generate the files. The dist folder will contain my-lib.js (the Custom Element), and separate react, angular, and vue directories with wrapper modules.
Consuming the Component in React
React wrappers expose a JSX component that forwards props, events, and slots to the underlying Custom Element. Import the wrapper in a React app:
import { MyButton } from 'my-lib/react';
function App() {
return (
console.log('clicked')} label='Press me' />
);
}
Key points:
- No Stencil runtime is bundled; the wrapper only proxies.
- Tree‑shaking works because the wrapper is a static module.
- Props are typed via the generated TypeScript definitions.
Consuming the Component in Angular
Angular wrappers generate a module that declares a component proxy. Import it in app.module.ts:
import { MyButtonModule } from 'my-lib/angular';
@NgModule({
imports: [MyButtonModule]
})
export class AppModule {}
Usage in a template:
<my-button [label]="'Click me'" (click)="onClick()"></my-button>
Consuming the Component in Vue
Vue wrappers expose a component that registers the Custom Element in the components object:
import { MyButton } from 'my-lib/vue';
export default {
components: { MyButton }
};
Template usage:
<my-button label="Press" @click="handleClick" />
Consuming the Component in Vanilla HTML
Include the bundled file in a <script> tag:
<script type="module" src="./dist/my-lib.js"></script>
<my-button label="Hello"></my-button>
The Custom Element is registered automatically by the bundle.
Trade‑Offs & Limitations
| Aspect | Pros | Cons |
|---|---|---|
| Shadow DOM | Encapsulated styles, no leakage. | Cannot pierce styles from host; may require scoped or none for theming. |
| Framework Wrapper API | Native look‑and‑feel, tree‑shaking. | Wrapper names may change across Stencil or framework releases; upgrades can break consumer code. |
| SSR/Hydration | Supports prerendering with prerender target. |
Requires matching hydrated flag; mismatches cause duplicate markup. |
Practical Checklist
- Verify
outputTargetsinstencil.config.ts. - Run
npm run buildand inspectdistfor framework folders. - In a host app, import the wrapper and test prop passing and event emission.
- Check bundle size: ensure no Stencil runtime is present in the wrapper output.
- For SSR, confirm that the prerendered markup matches the client hydration.
Conclusion
Stencil’s output targets give design system engineers a single source of truth that ships to React, Angular, Vue, and vanilla apps without runtime adapters. By configuring outputTargets, you can control style encapsulation, wrapper generation, and bundle size. The trade‑offs—shadow DOM, wrapper versioning, and SSR alignment—are manageable with careful version pinning and testing. Adopt this pattern to reduce duplication, simplify upgrades, and deliver a consistent component experience across ecosystems.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.