Svelte 3 has a small API surface and allows you to create components as simple as you want. You can also create complex components with a bit more effort. Reusable components is one of the goal’s of all major frontend framework and Svelte is no stranger to this. When I talk about reusable components I mean components that you can define what action it should take and how it looks.
So these are concepts that other frameworks highlight very well and have tools to help with, in Svelte we don’t really need tools as such but we need to have a better understanding of how this UI framework works. In this post we will create a simple button component, then we will build on that to make it a reusable component.
A component can be as simple as just some HTML in a .svelte
file.
HTML
DefaultButton.svelte
<button>
Hello World
</button>
This is the simplest form of a Svelte component, but this isn’t really useful since it’s not performing an action. So let’s add an action to this button by adding a on:click
directive to our code with an inline function.
<button on:click={() => alert('Life has never Svelte better')}>
Hello World
</button>
CSS
How about adding some styling to this button to give it a bit of swag (is this still a word people use?). Because Svelte’s philosophy is to build on the knowledge you already have, you can write CSS in a regular <style>
tag.
<button on:click={() => alert('Life has never Svelte better')}>
Hello World
</button>
<style>
button {
color: #fff;
background-color: #333;
border-radius: 5px;
padding: 8px;
border: none;
}
</style>
Now we have a styled button, but you might be asking “Won’t the button
style affect all buttons on a page if we should use this button component elsewhere?”, no it won’t, as styles inside of components don’t leak (in other words, styles are scoped) unless you use the :global
modifier before the name of the styling. We will use this :global
modifier later on.
Our simple component is done, now how do we allow someone to use this DefaultButton
component while being allowed to change its behaviour and look without actually editing the existing DefaultButton
component.
We can start with the look of the button. We will create a new component called Twitter
and inside of this component, we will import our original DefaultButton
component.
Twitter.svelte
<DefaultButton />
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
Whoa, hold up, what’s this <script>
tag doing here and why does it have an import
inside of it? OK the <script>
is yet again something you might already be familiar with, but the import might be new to you, let’s just say this is how Svelte loads its component into a new file.
Let’s try to customise some of the CSS to make this button look more Twitter like.
Twitter.svelte
<DefaultButton />
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
<style>
button {
background-color: #1DA1F2;
}
</style>
Notice, this had no effect at all and the Svelte compiler will tell you that there is an unused CSS selector in the code and this is because styles are scoped to their component. Inside of this component, there is no button
HTML element for this CSS selector to apply to. Remember I mentioned the :global
modifier earlier, let’s make use of it here.
<span class="twitter">
<DefaultButton />
</span>
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
<style>
.twitter :global(button) {
background-color: #1DA1F2;
}
</style>
There are some subtle differences in this code compared to what we had previously, a <span>
tag has been added with the class name of twitter
. This is to help with specificity (yep, this is a word apparently) when attaching a style to the button. Now that we have our twitter
class name, we can use it as part of our selector before applying the :global
modifier on the button
. Now, this style won’t be truly global, it will still be scoped to this component because Svelte will attach its own svelte-*
class to each HTML element generated.
Using props for content
But wait, our buttons are still saying Hello World. Surely the Twitter button should say Twitter and so on for the likes of any variation of this button while keeping the default of Hello World if we don’t change the text in the new button component we create.
We could create a prop on the DefaultButton
component and use the value as the placeholder name.
DefaultButton.svelte
<button on:click={() => alert('Life has never Svelte better')}>
{name}
</button>
<script>
export let name = 'Hello World';
</script>
<style>
button {
color: #fff;
background-color: #333;
border-radius: 5px;
padding: 8px;
border: none;
}
</style>
Then to use this we will need to update the Twitter
component.
Twitter.svelte
<span class="twitter">
<DefaultButton name="Twitter" />
</span>
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
<style>
.twitter :global(button) {
background-color: #1DA1F2;
}
</style>
But this will make the content of the button be constrained to a limited amount of data and the content would have to be in string format to use it.
Using slots for content
Svelte’s has an answer to this, a thing called slots, think of these as placeholder elements that can be replaced by just passing content inside of the component’s tags.
DefaultButton.svelte
<button on:click={() => alert('Life has never Svelte better')}>
<slot>Hello World</slot>
</button>
<style>
button {
color: #fff;
background-color: #333;
border-radius: 5px;
padding: 8px;
border: none;
}
</style>
With our code now using a slot
, we can add content directly inside of our DefaultButton
tags whenever we use it inside of another component.
Twitter.svelte
<span class="twitter">
<DefaultButton>Twitter</DefaultButton>
</span>
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
<style>
.twitter :global(button) {
background-color: #1DA1F2;
}
</style>
And now we can put any type of HTML content inside of the tag.
Twitter.svelte
<span class="twitter">
<DefaultButton>
<svg height="10" viewBox="328 355 335 276" xmlns="http://www.w3.org/2000/svg"><path d="M630 425a195 195 0 01-299 175 142 142 0 0097-30 70 70 0 01-58-47 70 70 0 0031-2 70 70 0 01-57-66 70 70 0 0028 5 70 70 0 01-18-90 195 195 0 00141 72 67 67 0 01116-62 117 117 0 0043-17 65 65 0 01-31 38 117 117 0 0039-11 65 65 0 01-32 35z" fill="#fff"/></svg>
Twitter
</DefaultButton>
</span>
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
<style>
.twitter :global(button) {
background-color: #1DA1F2;
}
</style>
The last thing you will notice is that when we click the button we are getting the same message from the underlying DefaultButton
component. But to gain full reusability, we need to also control the action that takes place when the button is clicked. We can achieve this two different ways, either by using an event dispatcher or a prop on the component. I will be using the prop method for this example.
Similar to what we did earlier when we created the name
prop in the DefaultButton
component, here we will create a buttonAction
prop for the consumer of this button to use, but we will also leave a default behaviour should the user decide to not set this prop.
DefaultButton.svelte
<button on:click={buttonAction}>
<slot>Hello World</slot>
</button>
<script>
export let buttonAction = () => alert('Life has never Svelte better');
</script>
<style>
button {
color: #fff;
background-color: #333;
border-radius: 5px;
padding: 8px;
border: none;
}
</style>
We can now make use of this in our Twitter
component by passing the action as a prop to the component.
Twitter.svelte
<span class="twitter">
<DefaultButton buttonClick={() => alert('I am going to Twitter')}>
<svg height="10" viewBox="328 355 335 276" xmlns="http://www.w3.org/2000/svg"><path d="M630 425a195 195 0 01-299 175 142 142 0 0097-30 70 70 0 01-58-47 70 70 0 0031-2 70 70 0 01-57-66 70 70 0 0028 5 70 70 0 01-18-90 195 195 0 00141 72 67 67 0 01116-62 117 117 0 0043-17 65 65 0 01-31 38 117 117 0 0039-11 65 65 0 01-32 35z" fill="#fff"/></svg>
Twitter
</DefaultButton>
</span>
<script>
import DefaultButton from './DefaultButton.svelte';
</script>
<style>
.twitter :global(button) {
background-color: #1DA1F2;
}
</style>
Even if our Twitter
component didn’t contain the buttonClick
we would still get the default alert
that was set up inside our DefaultButton
component.
Now we have a fully reusable DefaultButton
component that we can reuse to create new components. You could go ahead and create a Facebook
component and customise it as you see fit without affecting any of the existing button components you already created.