StencilJS: A Deep dive into the inner workings of output targets
Output targets, framework integration, static web app, things can get a bit hard to grasp with stencil. In this article, we’ll take a look at the output target types.

I decided to write this article after struggling for quite some time with our design system built using Stencil. Every component in our library was being included to our app’s production build, even if only a few components were used. Additionally, each app using a framework needed to call a function to initialize the components in the customElements registry. Needless to say, this fell short of the standards we aimed to achieve…
Everything I say in this article is based on my understanding and my experience, if I’m wrong or incomplete in something please go ahead and correct me.
If you’ve found your way here, I’ll assume you’re already familiar with StencilJS. If not, you can discover it on their documentation introduction: https://stenciljs.com/docs/introduction
You might also want to have a basic understanding of web components, which you can find here: https://developer.mozilla.org/fr/docs/Web/API/Web_components
Introduction on output targets

Stencil provide us a way to write custom web components in a single language and distribute those components to many different targets. They can be used directly within native HTML/JavaScript environments, or through wrappers tailored for today’s predominant JavaScript frameworks such as React, Angular, and Vue.
Stencil also facilitates the generation of documentation with many possible target outputs like Markdown, JSON, VScode and more.
In the Stencil configuration file, you can specify the desired outputs by populating the ‘outputTargets’ array with different configurations.
Different types of output targets just for web components
When you start looking into the documentation, you’ll find 3 target outputs types: dist, dist-custom-elements and www which are the core of stencil configurations.

If you’re here because you want to build a reusable component library for all main frameworks, you’re probably wondering what this is all about !
A core concept to understand is that every component you build will be a web component. The final framework library will just be a wrapper around those web components.
In your react/angular/vue app when you import your component, it’s using a web component in the background.
If I zoom in on my previous schema for the different frameworks, it would look something like that.

Alright! Now that we understand this, let’s deep dive on the behavior of the different output target types.
1 — Webapp Output Target: WWW
To quote the documentation, the
www
output target type is oriented for web apps and websites, hosted from an http server.
While Stencil is mostly known for it’s reusable component library capabilities, it can also be used to build full web app with JSX, virtual dom, store management and more.
You might be wondering why you would need this in your day to day component library work. Well, as the documentation says: “the www
output target is useful to build out and test the components throughout development.”. Let’s see how.

In the src folder, you have an index.html file. In a webapp context, this would be the entry point to load your root component but in a library context, it will be your testing environment.
When you run “npm start”, it will build the WWW output target and launch the index.html in your web browser. It is implemented by default with live reload so you can work on your component in the TS file and test it’s implementation in the index.html.

The result of the www build will be a new “www” folder at the root of your project with:
- index.html: root file of the webapp.
- build folder: every component separated in their own file for lazy loading + global files.

You can find more configuration on the documentation page or a webapp example on github.
2— Distribution Output Target: dist
Dist is the default component distribution output target. It’s easy to use and technical part like lazy loading and tree shaking are handled by Stencil.
You can use the default configuration and already have a full library with each component in a separated file to be lazy loaded and available in many different form.


What you can find in the build folder:
- cjs and esm folder: bundler ready javascript file for commonjs and modules.
- collection: Stencil files transpiled to simple javascript. Those are the files used if you import your library into another stencil distribution.
- loader: bundler entry for lazy loaded builds (used for example to register all components at once in the web component registry with defineCustomElements function).
- output-target-stencils (app workspace name): browser ready component.
- types: typescript types with d.ts files.
Let’s try it out !
To prevent CORS errors in chrome, we launched a small server using the serve library at the root of our folder. We get the localhost:3000 url for our project.
There is different way of using the component build with the dist output.
First one is using the browser ready components and importing them directly in the <head> tag.

It will register all the components to the customElements registry.

But only the one used in the index.html is being loaded in the browser thanks to the lazy loading.

Second one is to use the loader, it brings the same result. Components are lazy loaded.

Finally, you can push your library as a package on npm and import it to your project. I won’t be showing an exemple here but you can follow the documentation: https://stenciljs.com/docs/distribution#importing-the-dist-library-using-a-bundler
3 — Custom Elements: dist-custom-elements
This last output target, I will define it as: Get the minimum and do everything yourself.
You get to personalize everything in the behavior of the custom elements: when you want to register them, how the lazy loading works, the tree shaking etc… (well you personalize the stencil configuration and your bundler, like webpack or vite, will do the heavy lifting).
As the documentation says: This output target excels in use in frontend frameworks and projects that will handle bundling, lazy-loading, and custom element registration themselves.
Let’s see why it would still be a good idea to use it.
As with the previous one, let’s start with the minimal config for dist-custom-elements.

The dist folder is way smaller than for the “dist” output target, no esm/cjs split. No collection to reuse on another stencil project, just the components in separated files.

For this output target, we have to start looking at the code of one of the component to understand the usage.

We can see that each component bundle it’s own CSS.

Here we can see the /*@__PURE__*/ annotation that will indicate to the bundler that this component is pure, which means that if it not being called directly by your app. It can safely be ignored when bundling (tree shaking).

Each component export it’s own defineCustomElement function so we can choose to register each component one by one.
Let’s play with the configuration a little bit. For the component to work in a standalone index.html like before I need to set the configuration for externalRuntime to false because imports like “@stencil/core” don’t work without a bundler.
So without any other configuration (expect externalRuntime: false), I need an html like below to use the component.

And as expected, only my-component is registred to the customElements registry.

We can play with the customElementsExportBehavior configuration option.
- auto-define-custom-elements : the defineCustomElement function will be directly called inside the component file so you just have to import the file for the component to work.

- bundle : will make available, like in the dist output target, a defineCustomElements function in the index.js file to register all components at once

- single-export-module: allow you to import all component from the index.js file.

Those configuration possibilities for dist-custom-elements will get really interesting when you start adding framework’s output target: https://stenciljs.com/docs/overview
Conclusion
StencilJS is a great framework because it allows projects flexibility in their configuration and you can generate packages for all the possible usage. It’s especially great because you can build your components library in all the output targets possible and let the application using your library decide on how they want to import it.
It can get messy in the configuration between the different output-target for web components and a little hard to grasp the difference between dist and dist-custom-elements.
Since this article was getting pretty long, I’ve decided to put the information about framework integration in another article where I will provide a GitHub repository for a complex stencil setup with custom framework integration.
Stay tuned…
EDIT: here is the second part => https://alexandreolive.medium.com/unlocking-cross-framework-power-stenciljs-configuration-demystified-cd12933b1aaf
Alexandre Olive — If you liked this article, please don’t hesitate to follow me here on medium. On X (twitter) or add me on Linkedin. I write about Web, Video processing and Health/Sport.