Build your own component library with Svelte
Tags: svelte, webdev, tutorial, programming
Published: 2021-11-09 17:15:46

**Written by [Madushika Perera](https://blog.logrocket.com/author/madushikaperera/)** ✏️

## Introduction

Svelte is an open source JavaScript component framework for building web applications. Svelte takes a different approach than all the existing web frameworks like React, Angular, and Vue, which follow declarative, state-driven code.

The Svelte bundle size is significantly smaller than most frameworks because it doesn’t have any dependencies (only dev dependencies are available in the `package.js` file). Due to these features, Svelte has become [the most loved, dreaded, and wanted web framework in 2021](https://insights.stackoverflow.com/survey/2021#section-most-loved-dreaded-and-wanted-web-frameworks).

Due to this popularity, devs have introduced several awesome UI component frameworks/libraries like [Svelte Material UI](https://sveltematerialui.com/), [Smelt](https://smeltejs.com/), [Svelte Materialify](https://svelte-materialify.vercel.app/) and [Sveltestrap](https://sveltestrap.js.org/).

But what would it be like to build your own Svelte component library? Fortunately, there are several templates that you could start with, like the official [template provided by Svelte](https://github.com/sveltejs/component-template) and [Svelte 3 Component Template](https://github.com/YogliB/svelte-component-template), which are considered the go-to for building your own component library.

However, these templates are highly opinionated, and you may not be able to see the underlying tools and techniques required to build the component library itself. In this article, we are going to learn how to build a component library on our own using Svelte’s new SvelteKit.

## What is SvelteKit?

SvelteKit can be considered the successor to [Sapper](https://sapper.svelte.dev/) or NextJS for Svelte. It is packed with tons of cool features, like server side rendering, routing, and code splitting.

SvelteKit uses [Vite](https://vitejs.dev/) under the hood, which is quite surprising, as Sapper and most tools are developed using [Snowpack](https://www.snowpack.dev/). Vite 2 is framework-agnostic and designed with SSR at its core.

SvelteKit is still in beta, but it’s very stable, and there are many projects in production using this framework.

## Getting started with SvelteKit

For this project, we will use a skeleton project as the base for our library.

Let’s initialize the project using SvelteKit. You will need to execute the following command and select the options given by the Svelte:

![Initializing sveltekit](https://blog.logrocket.com/wp-content/uploads/2021/10/Initializing-sveltekit.png)

## Integrate Storybook

It’s time to integrate Storybook, an open source tool for building UI components and pages in isolation. It streamlines UI development and testing, which is ideal for our component library development. It allows us to build components without worrying about the configurations or development server in SvelteKit.

In your SvelteKit project root, execute the following command. This will identify and generate the necessary configurations for Svelte:

```shell
npx sb init
```

You may face some issues when setting up Storybook with a SvelteKit project. Storybook will throw an error when you start the server, like so:

![storybook error](https://blog.logrocket.com/wp-content/uploads/2021/10/storybook-error.png)

This issue is thrown due to the property of the `“type”:”module”` under the `package.json` file, which means we cannot use ESM-required syntax.

To overcome this, you can make a small tweak in the Storybook configuration files. Simply change the extension of your Svelte Storybook configuration files from `.js` to `.jcs` and inside the `main.cjs` file, make sure you comment out the `svelteOptions` property, which consists of a `require` command.

![Storybook configuration files](https://blog.logrocket.com/wp-content/uploads/2021/10/storybook-configuration-files.png)

After making the above tweaks, you can run the command below to start the Storybook server:

```shell
npm run storybook
```

This command will open up a new tab in the browser that loads the Storybook app for our SvelteKit project.

![Storybook welcome page](https://blog.logrocket.com/wp-content/uploads/2021/10/Storybook-welcome-page.png)

## Factors to consider before building components

Consider the factors below before building components, as they will help us follow the proper guidelines.

### Props usage

The word “props'' is common to all major frameworks and libraries like Vue and React. Props pass data to child components or enable component communication.

### Slots and `$$slots` usage

Even though props allow you to reuse components by passing data, it will bring a strict parent-child relationship. This means its HTML content will always control the child, and the parent is only capable of passing different values, so components can’t be composed together with props.

This is where slots come in handy. Slots maintain reusability while allowing the parent component to control the content of the child, including the HTML elements inside it. By adding `<slots/>` tags, you can pass down HTML or markdown from the parents, not just the values.

### Avoiding nested and global CSS

When building components, avoid nested and global CSS because they will not be scoped, which means they will leak through to all child components.

### Handling events

When building components, make sure you add or handle the proper events. You will have to use a Svelte API called `createEventDispatcher`, which can be useful when dispatching events.

## Creating components

Let’s create some components for our library. First, remove the default `story` folder generated by Storybook, and create a `stories` file under the `src` directory.

Next, we will focus on the project structure. Create a directory called `lib` under the `src` directory. This `lib` directory is a special directory for SvelteKit because it will allow us to use a special notation called [`$lib`](https://kit.svelte.dev/docs#modules-$lib). `$lib` can be used to alias the `src`/`lib` directory and helps us to access components and utility modules without using relative paths like `../../../../ .`

### Button component

Now let’s create our button component under the `lib` directory called `Button.svelte`:

```svelte
<script>
  import { createEventDispatcher } from 'svelte';

  export let primary = false;
  export let size = 'medium';
  export let label = '';

  const dispatch = createEventDispatcher();
   /**
   * Button click handler
   */
   function onClick(event) {
    dispatch('click', event);
  }
</script>

<button
  type="button"
  class={['sveltio-button', `sveltio-button--${size}`,
   `sveltio-button--${primary?'primary':'secondary'}`].join(' ')
   }
  on:click={onClick}>
  {label}
</button>

<style>
.sveltio-button {
    font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 700;
    border: 0;
    border-radius: 3px;
    cursor: pointer;
    display: inline-block;
    line-height: 1;
}
.sveltio-button--primary {
    color: #1b116e;
    background-color: #6bedb5;
}
.sveltio-button--secondary {
    color: #ffffff;
    background-color: #1b116e;
    box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.sveltio-button--small {
    font-size: 12px;
    padding: 10px 16px;
}
.sveltio-button--medium {
    font-size: 14px;
    padding: 11px 20px;
}
.sveltio-button--large {
    font-size: 16px;
    padding: 12px 24px;
}

.sveltio-button--primary :hover {
    color: #1b116e;
    background-color: #55bd90;
}

.sveltio-button--primary :active {
    color: #1b116e;
    background-color: #55bd90;
    border: solid 2px #1b116e;
}

.sveltio-button--primary :disabled {
    color: #1b116e;
    opacity: 0.5;
    background-color: #6bedb5;
}

.sveltio-button--secondary :hover {
    color: #1b116e;
    background-color: #55bd90;
}

.sveltio-button--secondary :active {
    color: #1b116e;
    background-color: #6bedb5;
    border: solid 2px #1b116e;
}

.sveltio-button--secondary :disabled {
    color: #ffffff;
    opacity: 0.5;
    background-color: #1b116e;
}

</style>
```

Notice that we have added styles in the same file under the `<style>` tags.

Now let’s create a stories file for our Button component called `Button.stories.svelte`:

```svelte
  <script>
    import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
    import Button from "./Button.svelte";
  </script>

  <Meta
    title="Sveltio/Button"
    component={Button}
    argTypes={{
      label: { control: "text" },
      primary: { control: "boolean" },
      backgroundColor: { control: "color" },
      size: {
        control: { type: "select", options: ["small", "medium", "large"] },
      },
      onClick: { action: "onClick" },
    }}
  />

  <Template let:args>
    <Button {...args} on:click={args.onClick} />
  </Template>

  <Story
    name="Primary"
    args={{
      primary: true,
      label: "Button",
    }}
  />

  <Story
    name="Secondary"
    args={{
      label: "Button",
    }}
  />
  <Story
    name="Large"
    args={{
      size: "large",
      label: "Button",
    }}
  />

  <Story
    name="Small"
    args={{
      size: "small",
      label: "Button",
    }}
  />

```

Notice that we have created several templates for our button component by passing several arguments to the templates.

Now in the Storybook window, you will be able to see a button.

![Storybook button](https://blog.logrocket.com/wp-content/uploads/2021/10/Storybook-button.png)

You can switch from the **primary** button to the **secondary** button from the controllers provided below. You can also clearly view the types of events that are available for this custom component from the **Actions** logs.

### Toggle component

Now let’s create a toggle component. Start by creating the `Toggle.svelte` and `Toggle.stories.svelte` file:

`Toggle.svelte`:

```svelte
<script>
    export let label = '';
    export let isToggled = false;
    export let style = '';
</script>

<label {style} class="sveltio-toggle-label">
    <input type="checkbox" class="sveltio-input" bind:checked={isToggled} />
    <div class="sveltio-toggle" />
    {label}
</label>

<style>
    .sveltio-toggle-label {
    --width: 40px;
    --height: calc(var(--width) / 2);
    --radius: calc(var(--height) / 2);
    display: flex;
}

.sveltio-toggle {
    position: relative;
    width: var(--width);
    height: var(--height);
    border-radius: var(--radius);
    border: solid 1px #c2c2c3;
    transition: background-color 0.3s ease;
    margin-right: 5px;
    background-color: var(--toggleBackgroundColor, #c2c2c3);
}

.sveltio-toggle::after {
    content: '';
    position: absolute;
    top: -1px;
    left: -1px;
    height: var(--height);
    width: var(--height);
    border-radius: var(--radius);
    background-color: var(--toggleButtonColor, #ffffff);
    box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
    transition: transform 0.3s ease;
}
.sveltio-input {
    display: none;
}

.sveltio-input:checked + .toggle {
    background-color: var(--toggleCheckedBackgroundColor, #1b116e);
}

.sveltio-input:checked + .toggle::after {
    transform: translate3d(100%, 0, 0);
}

</style>
```

`Toggle.stories.svelte`:

```svelte
<script>
    import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
    import Toggle from "./Toggle.svelte";
  </script>

  <Meta
    title="Sveltio/Toggle"
    component={Toggle}
    argTypes={{
      label: { control: "text" },
      primary: { control: "boolean" },
      backgroundColor: { control: "color" },
      size: {
        control: { type: "select", options: ["small", "medium", "large"] },
      },
      onClick: { action: "onClick" },
    }}
  />

  <Template let:args>
    <Toggle {...args} on:click={args.onClick} />
  </Template>

  <Story
    name="Labeled"
    args={{
      primary: true,
      label: "Check me",
    }}
  />

  <Story
    name="Blank"
    args={{
      label: "",
    }}
  />
```

Now let's see how this component will be rendered in Storybook. This story consists of Two templates called **Labeled** and **Blank**, and will render two instances of the same component with different props or arguments.

![Storybook toggle component](https://blog.logrocket.com/wp-content/uploads/2021/10/Storybook-toggle-component.png)

### Input field component

Finally, we will create an input field component with styling that shows pleasing colors for each state for the input.

Write the following in your `Input.svelte` file:

```svelte
<script>
    import { createEventDispatcher } from 'svelte';

    export let placeholder = '';
    export let label = '';
    export let disabled = false;

    export let state = "active";

    const dispatch = createEventDispatcher();

     /**
     * input change handler
     */
     function onChange(event) {
      dispatch('click', event);
    }
</script>

<label >

    {#if label}
        <span class="sveltio-input-label">{label}</span>
    {/if}

    <input
    disabled={disabled}
    type="text"
    class={['sveltio-input',`sveltio-input--${state}`].join(' ')}
    placeholder={placeholder}
    >
</label>

<style>
    .sveltio-input {
    font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 700;
    min-height: 25px;
    margin: 2px;
    border-radius: 3px;
    border: solid 2px #353637;
    background-color: #ffffff;
}

.sveltio-input ::focus {
    border: solid 2px #1b116e;
}

.sveltio-input--success {
    border: solid 2px #067d68;
}

.sveltio-input--error {
    border: solid 2px #a9150b;
}

.sveltio-input--disabled {
    color: #e4e3ea;
    border: solid 2px #e4e3ea;
}

.sveltio-input ::-webkit-input-placeholder {
    color: red;
}

.sveltio-input-label {
    font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 700;
}

</style>
```

Now, because we have created the `input` component with its styling, let’s see how we can write stories for these components and how it renders. Here, we will pass in some arguments like `backgroundColor` and `state`.

`Input.stories.svelte:`

```svelte
<script>
    import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
    import Input from "./Input.svelte";
  </script>

  <Meta
    title="Sveltio/Input"
    component={Input}
    argTypes={{
      backgroundColor: { control: "color" },
      state: {
        control: { type: "select", options: ["active","success", "error"] },
      },
      onChange: { action: "onChange" },
      disabled:{ control: "boolean" }
    }}
  />

  <Template let:args>
    <Input {...args} on:change={args.onChange} />
  </Template>

  <Story
    name="Active"
    args={{
      placeholder: "Text Input Active",
      state:"active"
    }}
  />

  <Story
    name="Success"
    args={{
      placeholder: "Text Input Success",
      state: "success",
    }}
  />
  <Story
    name="Error"
    args={{
      placeholder: "Text Input Error",
      state: "error"
    }}
  />

  <Story
    name="Disabled"
    args={{
      state: "disabled",
      disabled:true
    }}
  />

```

In a similar manner, you can create other web components and create stories for other components to continue building your component library. You can find the whole code for the components we built through this [link](https://github.com/LMPerera/sveltio).

## Testing with svelte-testing-library and Jest

One of the most critical aspects of developing web applications is running and maintaining tests for our components. With Svelte, the process of running tests is similar to what we get with React, Vue, or Angular.

There are several tools to write and run tests like Mocha, Karma, Jasmine, and [Jest](https://jestjs.io/). For this project, we will be using Jest as our test runner. However, even Jest falls slightly short, because we need to render our component and check how it's behaving after executing actions on them.

For this purpose, we will be using a tool called [Testing Library](https://testing-library.com/). This tool helps us write tests as if a real user is working on the elements, and also supports all the major frontend frameworks and libraries.

We will also use an additional plugin with Testing Library called [user-event](https://testing-library.com/docs/ecosystem-user-event/), which allows us to mimic user events, like typing inside an input or clicking on a button. We will also use a plugin called [jest-dom](https://testing-library.com/docs/ecosystem-jest-dom/), which extends the DOM-related matching capabilities of Jest, which we require because we are dealing with web components.

Now let's install these libraries as dev dependencies in our project like so:

```shell
npm install --save-dev jest babel-jest svelte-jester  @testing-library/svelte @testing-library/user-event @testing-library/jest-dom @testing-library/dom
```

Now, let’s add some configuration files to the root of our project. Starting with the `Jest.config.cjs` file which holds the configurations for Jest and a `.babelrc` with some presets for converting the files to ES2015 JavaScript.

`Jest.config.cjs`:

```cjs
module.exports = {
    transform: {
        "^.+\\.js$": "babel-jest",
        "^.+\\.svelte$": "svelte-jester"
    },
    moduleFileExtensions: ['js', 'svelte'],
    moduleNameMapper: {
        '^\\$lib(.*)/pre>: '<rootDir>/src/lib$1',
        '^\\$app(.*)/pre>: [
            '<rootDir>/.svelte-kit/dev/runtime/app$1',
            '<rootDir>/.svelte-kit/build/runtime/app$1'
        ]
    },
    setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
    testEnvironment: "jsdom"
};
```

`.babelrc`:

```javascript
{
    "presets": [["@babel/preset-env", {"targets": {"node": "current"}}]]
}
```

The above test will check if the text is available in the button and do some assertions.

Let’s add another test for the `Input` file we have created. Let’s call this `Input.test.js` under the `test` directory:

```javascript
import '@testing-library/jest-dom';
import Input from '$lib/Input/Input.svelte';
import { render } from '@testing-library/svelte';

describe('Input component', () => {
    it('Input Has Placeholder', () => {
        const { getByPlaceholderText } = render(Input, { placeholder: 'Hello Sveltio' });
        expect(getByPlaceholderText('Hello Sveltio')).toBeInTheDocument();
    });
});
```

The above test will check if the input field consists of the placeholder we pass as a prop.

Before we run these tests, we will add a small attribute called `“test”:”jest”` under `scripts` to the `package.json` file.

Now all you have to do is run the following command in the project root:

![Jest button test](https://blog.logrocket.com/wp-content/uploads/2021/10/Jest-button-test-pass.png)

With this, you can maintain unit tests that will improve the quality of your components.

## Packaging and publishing to npm

Now it’s time to publish your project to the world! There are several tools that can be used to export your components as a package, but we will be using a cool feature that comes built in to SvelteKit. First, add this attribute to the `package.json` file:

```json
"package": "svelte-kit package"
```

Now all you have to do is run the following from the root of the project:

```shell
npm run package
```

> If you didn’t initialize a TypeScript SvelteKit project, you will need to install a dependency called `svelte2tsx`, which converts the Svelte component source into TSX.

This command will take all the files that are under the `src/lib` folder and make them available as a package. This command generates a new directory called `package` at the root of your project, and inside this, you will notice that there is a new `package.json` file. This file consists of an attribute called `exports`, which consists of all the paths or entry points to the individual components that we have developed.

Enter the following code into the `package.json` file under the `package` directory:

```json
{
  "name": "sveltio",
  "version": "0.0.1",
  "devDependencies": {
   //some dependencies
  },
  "type": "module",
  "dependencies": {},
  "exports": {
    "./package.json": "./package.json",
    "./Button.svelte": "./Button/Button.svelte",
    "./Input.svelte": "./Input/Input.svelte",
    "./Modal.svelte": "./Modal/Modal.svelte",
    "./Toggle.svelte": "./Toggle/Toggle.svelte"
  }
}
```

If your library consists of files like `src`/`lib`/`index.js` or `src`/`lib`/`index.svelte`, it will be treated as the package root. This makes it much easier to use the components of our library as ES module imports.

For example, if you had an `src`/`lib`/`Button.svelte` component and an `src`/`lib`/`index.js` module that re-exported it, a consumer of your library could do either of the following:

```shell
import { Button } from 'your-library';
```

or

```shell
import Button from 'your-library/Button.svelte';
```

Now that we have created a package using SvelteKit, it’s time to publish it as an npm module. All you need to do is to execute the following command from the root of the project:

```shell
npm publish ./package
```

The above command will publish the package we have created using SvelteKit. Make sure that the package name and package version combination don’t exist already; if so, the package will not be published on npm.

## Conclusion

You can see why SvelteKit deserves more attention in the developer community. SvelteKit has fantastic features packed in, and creating packages is quite easy. With tools like Storybook and Jest, creating a component in isolation and maintaining tests for components is simple and efficient. For more information, please go through the SvelteKit [documentation](https://kit.svelte.dev/docs).

---

## [LogRocket](https://logrocket.com/signup/): Full visibility into your web apps

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup/)

[LogRocket](https://logrocket.com/signup/) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

[Try it for free](https://logrocket.com/signup/).