Lorenz Weiß Hi, I'm Lorenz, a frontend-focused web developer. I'm in love with the internet and its people and interested in everything related to it.

Building Jamstack-friendly components with Tonic

4 min read 1391


The world of frontend web development changes quickly, thanks to new frameworks mixing up the market and new ideas and approaches evolving rapidly.

React and Vue, for example, were one of the first component-based frameworks, and they are currently the most widely used.

With the trend of Jamstack websites moving back to statically rendered websites instead of dynamically rendered websites on the client side, new frameworks built on top of React or Vue are gaining popularity, such as Next.JS, Gatsby, and Nuxt.

In addition, new frameworks like Svelte are being adopted by developers because they make it even easier to create statically rendered websites without losing the modern feel of dynamic frameworks like React.

What is Tonic?

Tonic is a component framework that takes this minimal yet elegant approach and works perfectly with Jamstack, as it doesn’t use virtual DOM and is server-side rendered by default. It’s based on web components and can be used out of the box without a build tool. It is super small (5KB!) and blazingly fast.

This article explains how Tonic works and why you should use it when building a Jamstack website. I will also show you how to set up a project and build your first components using Tonic.

How Tonic works

Tonic uses native web components, a concept introduced in 2011 that is now part of all modern browsers by default.

One of the most famous websites powered by web components is YouTube. If you look at the source code, you’ll quickly discover many components in the DOM:

Components in the DOM

Although using web components natively is possible, it can be complex and difficult to understand.

Because of this, Tonic has added some abstraction on top of web components that gives it a similar look to frameworks like React and Vue, but, aside from using virtual DOM to render elements, it actually uses the web components natively built into the browser.

Why use Tonic?

So, why use it when there are so many frameworks out there? Here are some great reasons you should use Tonic to create a Jamstack website:

  • It’s super small — basically non-existent. It’s only 350 lines of code and it’s 5KB in size, so your website will load quickly
  • You don’t need a build tool. Have you ever gotten lost in a webpack configuration or waited for your frontend build to finish? With Tonic, the code works out of the box, no build tools required
  • You can debug your production code as the web components appear in the HTML document of your production website, and you can see the names of your components in the DOM of your website
  • The developer experience is the same as other modern frameworks, so it’s plain-old fun to write your components in it

How to use Tonic

Now that we’ve seen the benefits of Tonic, let’s bootstrap a project using it. Our goal is to have a page that contains a counter button and a number next to it that increments every time we click the button.

Because we don’t need a build tool, we can create a simple HTML file that contains:

<!doctype html>  
    <title>Website with Counter</title>  
      <script src="https://unpkg.com/@optoolco/[email protected]/dist/index.js" ></script>  
    <script type="module" src="index.js" ></script>  
    <script src="https://unpkg.com/@optoolco/[email protected]/dist/index.js" ></script>  
    <link rel="stylesheet" type="text/css" href="index.css" media="screen" />  

Let’s go through the parts we have in the head tag. We have the title of our website, and we need a script tag to import the code from Tonic. We use the unpkg tool to import it with a script tag.

The next script tag is for importing our custom JavaScript file we will create with no content for now. We also want some styles, so we’ll create a style.css file and link it as a stylesheet.

The body part will contain our custom counter element. We’ll leave it empty for now and fill it in when we write the actual component. Now, we should have the following classic web development files:

-- index.css
-- index.js
-- index.html

And that’s it! You don’t need any other files or a node modules folder. However, if you want to run it on a development server, you can initialize it as an npm package and use serve to run your website on localhost by adding the following package.json file:

// package.json
     "name": "tonic-proj",  
     "version": "1.0.0",  
     "description": "",  
     "main": "index.js",  
     "scripts": {  
         "start": "serve"  
     "keywords": [],  
     "author": "",  
     "license": "ISC",  
     "dependencies": {  
         "serve": "^13.0.2"  

Then, after running npm i and npm start, the project should open on localhost:3000. This should be a pretty static hello message.

Creating the first Tonic component

So far, there’s nothing special here. But to add the actual counter, we need to write our first Tonic component.

We have to first get the Tonic class from the imported package. At the top of the index.js file, let’s add the following:

const components = window.components;  
const Tonic = components.Tonic;

Then, we can create our first component that extends the Tonic class:

// ...

class CounterElement extends Tonic {  
    state = { counter: 0 };

    click(e) {
        const trigger = Tonic.match(e.target, "button");
        if (trigger) {

    render() {  
        if (typeof this.state.counter === "undefined") {  
            this.state.counter = 0;  
        return this.html`
                <p>Count: ${this.state.counter.toString()}</p>

As you can see, it looks similar to React code and the JSX pattern. Similar to React, you need a render function in your class that returns the return value of the inherited function this.html. This function accepts a string containing the HTML elements you want your component to render.

If you want to render dynamic values, you can simply add them with a template string. The component also accepts a set of event methods that are fired when the HTML element events are triggered.

In our example, we use the click event to listen for when someone clicks on our component. Because we want the count to fire only when we have actually clicked on the button, and not on the entire component, we add a matching check for this in the click method. This is because of Tonic’s decision to use event delegation instead of multiple event listeners.

To wrap up the JavaScript piece, we also need to add this component to Tonic by adding the following to the end of our JavaScript file:

// all the above code...


Don’t panic that nothing is displayed on your website yet. We need to add the element to the HTML file by adding it as a web component that Tonic automatically creates for you.


Now, you should be able to see your counter element in the DOM, and you should even be able to see it when you inspect the DOM of the site.

Counter Element

Here, you can see a fully functional example of the component we just created:

See the Pen
by lowe1111 (@lowe1111)
on CodePen.


Writing components in Tonic feels pretty similar to writing components in other frameworks. In particular, the class-based approach is very similar to writing class components in React. You also have lifecycle methods that are automatically triggered when the component is updated or rendered, and you return HTML elements in a render function.

The major difference is that Tonic uses your exact written code, and the DOM structure matches exactly what you have on the server and what the user sees in their browser.

This means that you can create Jamstack components without having a build tool or rendering engine installed on the server side. It uses the power of web components.

And, although the developer experience is pretty similar to React or other modern frameworks, using Tonic is much more streamlined, the debugging feels simpler, thanks to the lack of build tools, and because of static rendering, Tonic also loads and renders lightning fast. Happy hacking!

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Lorenz Weiß Hi, I'm Lorenz, a frontend-focused web developer. I'm in love with the internet and its people and interested in everything related to it.

Leave a Reply