Implementing a Design Language System with Stencil.js

Hello Web Components!

Arnaud Tanielian
9 min readMay 20, 2020

In June of 2019 the Ionic team released the anticipated Stencil One, finally releasing a solid library to build Web Components, or more specifically, Custom Elements.

Perfectly in time for a type of project we see more and more at Work & Co: designing and developing a design language system.

A design language system, or DLS, is a broad term encompassing design patterns, principles and style guide. From a development perspective, it’s a collection of reusable functional components (link, button, form inputs, etc.), as well as “styles” definitions, such as a color library, typography system, spacing, etc.

Companies are integrating DLS in their ecosystem more and more, because it offers consistency and standards across products and projects. It’s a “source of truth” everyone trusts and refers to. More than this, it’s about owning a brand, sometimes even promoting it.

When it comes to building a design language system, keep in mind that it includes different elements. The foundations of a DLS include:

  • Grid, Layout system
  • Typography
  • Color palette
  • Breakpoints
  • Spacing rules
  • Atomic, composite components
  • etc.

The goal is to create a separated library, easily importable and usable in multiple projects.

The client we worked with is a huge 60+ year-old global company, made up of different teams working on a variety of digital products and using different frameworks and technologies.

After conducting stakeholder interviews, it was clear we needed the library to be easily and quickly adopted, across teams working with different libraries and frameworks (mostly Angular, but some React here and there, or even pure HTML/CSS/JS. Yeah I know, crazy in 2020).

Why Stencil.js ?

“Why” is by far the most important question.

The main reason is being framework agnostic.

A core value we believe in is promoting flexibility and extensibility in software architecture when possible. In this case, a user interface can be leveraged across multiple contexts: rendered client-side or server-side, integrated on top of popular JavaScript frameworks using a standard API, etc. It allows the UI to evolve without a tight coupling to the underlying implementation. While it’s not a fit for all clients, for those who require this level of adaptability, it is an ideal solution.

React, Vue, Angular, or JS only, all those setups are great for consuming component libraries made with Stencil, because it leverages Web Components with polyfills and fallbacks. The best part? It’s really, really simple to setup.

Using your Stencil library in React: literally 2 lines

After that, no need to import anything: the whole “catalogue” of components is directly available everywhere in your JSX render functions.

The second reason is the build files Stencil generates. The output is optimized and ready to be published on npm (or any other private library repository). Stencil has been designed in a way to build such a component library. npm run build, npm publish and voilà!

Finally, Stencil provides more features than you can think of. For instance, they provide a renderToString() method for SSR your component library. Its API lifecycle makes you feel comfortable, you’re not even thinking of the Web Component API. You can use the CLI to create a new component, generating JS, CSS, even E2E test files… And it automatically creates a documentation for each component. Just because. As you can imagine, you can easily add more on top of the main setup: Typescript, Storybook, Unit testing… You name it.

So... Web Components?

A web component gives a developer the ability to define a component with its own render, style and logic encapsulated. You can create a custom element using <template> elements, and leverage the Shadow DOM API that allows styles, ids and classes of the content to be scoped to itself.

That’s cool, but pretty painful to setup manually… This is where Stencil comes in handy:

A simple button component, with a little (useless) functionality using an internal @State() , allowing you to count and display the number of clicks.

If you’re familiar with a library like React, you’re already feeling good about this: A lifecycle API, a render function, states, props, SCSS (optional)…

Then, comes the time to use your component freshly created:

Easy peasy lemon squeezy.

And that’s it! Depending on if you want to render your component encapsulated using Shadow DOM or not, the HTML rendered is what you would expect from a classic React component render.

And there’s more: <slot> is a really interesting tag, allowing you to render one or multiple children within your component. You can use @Method() to expose public methods, accessible on an instance of your custom element. Or add a @Watch() to literally “watch” the value of a prop and implement a specific logic, depending of your needs (testing/updating the value, triggering an event…).

There are some gotchas, obviously. In a pure JS environment you can use numbers, strings, booleans or functions as props (because JSX), but because of the nature of custom elements they can only accept strings or booleans. They are HTML elements after all, with attributes. No functions, but more especially, no arrays or objects. But there are some workarounds, which we have found to be good solutions.

Some good (best?) practices

Web components are new, still young. Stencil, even more so. Still, when it comes to creating a solid component library we wanted to establish some best practices.

Stencil provides some good tips, but we had to identify practices that we think are good and could be considered sound longer-term.

Setup 2 environments: the library, and a testing one.

Implementing your library in Stencil allows you to test your components directly in an HTML file, but not in the real context of an app. That’s why the first thing we did was to actually create 2 projects:

  • Stencil project, or UCL (UI Component Library)
  • A basic Angular project, or RIE (Reference Implementation Environment)

For the RIE, we chose Angular because this is the environment that made the more sense for the client. It could be anything, even HTML only. The goal here is to be able to test the import and usability of your component library as a whole, in a real context.

Only issue here: In theory, you can’t import a component you’re working on in the RIE because it hasn’t been published yet, as part of the next library release.

In practice, there’s a little workaround: npm link . First, we would develop an alpha version of a component, in the Stencil project. The idea is to have a first pass and test the component in the Stencil environment.

Then, we would create a page dedicated to this component on the RIE side, on a separate branch. We would run local builds of the Stencil library and point to them with npm link. At that point, our local version overrides the imported node_module package in the RIE.

We would create different scenarios for this component on the RIE page, playing with different prop values, but more importantly, testing the component in terms of Javascript interactions (adding listeners, testing setter/getters…)

Once the component was fully built and part of the next library release, we would go back to the RIE component branch, bump the library version used by the project and open a PR with this new component page.

Based Trunk Development

We found the Based Trunk Development model to be a good source control model for the UCL, but also RIE project. The idea is to maintain a clean master, no stage or develop branch here. Every feature/chore/bug branches directly are from master , and only an approved PR by at least 2 developers, passing all the tests, could be rebased (not merged) with master .

Once we felt it was time for a new release, we would create a branch with the new release version (I.E. release/0.2.1 ), which then would be moving through different stages (development, QA, prod) before being publish in the main artifact repository (we used ADO).

This way, you keep a clean history of the evolution of your library, and every release branch are a snapshot of the library at the time of a precise release.

See https://trunkbaseddevelopment.com/ for more information about this model!

Custom fonts

Stencil is great, but nowhere is it told how to handle custom fonts, which is probably the #1 thing you do when you start a project like this.

While it’s not an issue when developing the library locally, the problems come when you’re actually using the library in another project: You need an online place to store your web fonts, so it’s accessible by the project using your library.

Therefore, the best candidate is a bucket (S3, Google Bucket, Azure storage…), probably with a CDN on top, to host your fonts or even more generally your static assets.

Then, include in your release pipeline/scripts the deployment of this static folder, so it’s accessible by your library in production.

Global component

Speaking of font-face declarations, we understood early on that it would be counterproductive to completely isolate every single component by scoping them to themselves with a Shadow DOM. We wanted to take advantage of the nature of CSS, not fight against it, and re-declaring font-faces on every component would just make those declarations repetitive.

So we had the idea of creating a global web component, in charge of injecting the font-face CSS declarations once, on every pages.

We realized this global component could take on more than just font-face declarations. Why not inject an optional opinionated CSS reset? An SVG Sprite we could use for a custom icon web component?

We found this to be an excellent idea in practice, and very elegant.

By adding this one tag, the global component injects the CSS rules you need in your document, once. While some component libraries (such as Material) make you add<link> elements to import CSS and fonts, this technique embraces the component logic, allowing you do to even more, with possible props rendering “sub” components.

For instance, we added a reset prop that injects an opinionated yet classic CSS reset. Not every project needs one, maybe your project has one already, but it’s nice to have the option.

Exposing JS/SCSS constants, utils, mixins…

As we were building the component library, we quickly saw that, from the point of view of a developer using the component library, it’s easy to forget about the possible values of a specific prop. Or simply having access to the constants used by the component library, such as the different breakpoint values, or the number of columns the grid uses.

To improve the developer experience, we decided to expose as much JS and SCSS constants as possible: colors, number of grid column, breakpoints, button themes…

After the Stencil build process is done, we would run our own scripts on top by concatenating constants, utils and mixins into more accessible files so one could easily import those values in a JS or SCSS environment.

In JS…
… but also in SCSS!

Icon component: SVG sprite FTW

When it comes to icons, one of the best web practices is to centralize them in an SVG sprite, and target a specific icon with the <use xlink:href="#icon-id-contained-in-sprite"> tag.

We’re first generating the SVG sprite using svg-sprite , as well as a dictionary of constants based on the icon filenames.

Then, injecting the SVG sprite is a perfect mission for our global component! We are using the stencil-inline-svg plugin which allows us to import and inject an SVG in a component with innerHTML.

Last step was to create the icon component, which turned out to be pretty simple: render in a container an <svg> element with the <use> tag described above, with a correct icon value, based on the icon filenames compiled into the sprite.

Passing data: Using the script tag

As we mentioned before, passing data to a web component can be challenging. We found a way that is elegant, yet not 100% optimized as we’re outputting the data in the DOM for the web component to consume.

The idea is to use a <script> element to pass our data, so we can retrieve them in the web component.

But some limitations too

When it comes to create a component library, you might wonder what to include in it. Using Web components, even with a library such as Stencil, could limit the type of components you want to develop.

We found it was difficult to create composite components. The main reason?
It’s difficult to pass down pure data — not impossible, but could be
quite heavy for the page. We tried other approaches, but they would
require developers to parse the data and create from scratch the
elements our web component would consume, which would go against
the benefits of it.

Based on our experience, we recommend developing only atomic components (on top of the grid system, fonts…). More complex ones (carousel, navigation…) should be done in the final environment (React, Angular…), using the atomic ones.

New kids on the block

We’re really excited about the perspective of using Web Components more and more in the future. It’s definitely getting more attention, features, and as we’re seeing with Stencil, more standards and support. There’s still room for improvements, and cannot wait to see new standard and initiatives push forward!

“You cannot tell but I am giving a thumbs-up”

--

--

Arnaud Tanielian

Also known as Danetag on the Internets. Engineering Manager @ Shopify