Building a tabs component with Svelte
If you ever build an application, web or native, then there’s a good chance you’ve seen a tabbed interface.
Even the bottom navigation of Instagram can be seen as a tabbed interface.
Tabs make it easy to explore and switch between different views.
In this article you will learn how to create a simple, and reusable Svelte tabs component.
You’re going to create 2 components.
The first Svelte component will be called Tabs
.
Tabs
will display a list of clickable tabs. It will hold it’s own state for which active tab is active, and it will pass the active tab value to the parent component.
The second Svelte component will be called App
.
The App
component will utilize the Tabs
component and delegate what view to display depending on which tab is active.
Let’s get started.
Building the Tabs Svelte component
<script>
import { onMount } from "svelte";
export let items = [];
export let activeTabValue;
onMount(() => {
// Set default tab value
if (Array.isArray(items) && items.length && items[0].value) {
activeTabValue = items[0].value;
}
});
const handleClick = tabValue => () => (activeTabValue = tabValue);
</script>
<ul>
{#if Array.isArray(items)}
{#each items as item}
<li class={activeTabValue === item.value ? 'active' : ''}>
<span on:click={handleClick(item.value)}>{item.label}</span>
</li>
{/each}
{/if}
</ul>
<style>
ul {
display: flex;
flex-wrap: wrap;
padding-left: 0;
margin-bottom: 0;
list-style: none;
border-bottom: 1px solid #dee2e6;
}
span {
border: 1px solid transparent;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
display: block;
padding: 0.5rem 1rem;
cursor: pointer;
}
span:hover {
border-color: #e9ecef #e9ecef #dee2e6;
}
li.active > span {
color: #495057;
background-color: #fff;
border-color: #dee2e6 #dee2e6 #fff;
}
</style>
This is a lot to digest. I’ll walk you through the code.
Let’s take a look at the <script>
tag logic first.
<script>
import { onMount } from "svelte";
export let items = [];
export let activeTabValue;
onMount(() => {
// Set default tab value
if (Array.isArray(items) && items.length && items[0].value) {
activeTabValue = items[0].value;
}
});
const handleClick = tabValue => () => (activeTabValue = tabValue);
</script>
First I’m importing the onMount
hook from Svelte, but I’ll explain why in a bit.
I’m then creating 2 component props for Tabs
.
The first component prop is items
.
items
will be a an array of tab navigation items.
The other component prop in Tabs
is activeTabValue
.
This an important property because it will hold the value of the active tab item, and it will toss the value back to the parent component to dictate what to display to the user.
Okay, back to onMount
. onMount
is critical here as well because I’m setting the first tab item as the default active tab.
I’m also creating a click handler function called handleClick()
.
The job for that function is update the value of activeTabValue
.
Now let’s look at the HTML.
<ul>
{#if Array.isArray(items)}
{#each items as item}
<li class={activeTabValue === item.value ? 'active' : ''}>
<span on:click={handleClick(item.value)}>{item.label}</span>
</li>
{/each}
{/if}
</ul>
This part is pretty simple. All I’m doing is adding a if-block conditional to make sure items
is an array.
I’m also adding logic to the class property on the li
element.
<li class={activeTabValue === item.value ? 'active' : ''}>
I’m just checking whether I need to add the active class name to the element.
The rest is just CSS styles.
Now, on to the App
Svelte component!
Building the App component
This Svelte component is the main view for the user, and will utilize Tabs
to display the menu.
It’s also responsible to show the appropriate content.
<script>
import Tabs from "./Tabs.svelte";
// List of tab items with labels and values.
let tabItems = [
{ label: "Tab 1", value: 1 },
{ label: "Tab 2", value: 2 },
{ label: "Tab 3", value: 3 }
];
// Current active tab
let currentTab;
</script>
<Tabs bind:activeTabValue={currentTab} items={tabItems} />
<code class="language-text"></code>
{#if 1 === currentTab}
<h3>Tab 1 content</h3>
{/if}
{#if 2 === currentTab}
<h3>Tab 2 content</h3>
{/if}
{#if 3 === currentTab}
<h3>Tab 3 content</h3>
{/if}
Code breakdown!
Starting with the <script>
tag.
<script>
import Tabs from "./Tabs.svelte";
// List of tab items with labels and values.
let tabItems = [
{ label: "Tab 1", value: 1 },
{ label: "Tab 2", value: 2 },
{ label: "Tab 3", value: 3 }
];
// Current active tab
let currentTab;
</script>
Other than importing the Tabs
component, I’m creating a variable called tabItems
.
tabItems
is what creates my tab menu.
I’m also creating a state property called currentTab
.
currentTab
will hold the value of the active tab inside the Tab
component.
{#if 1 === currentTab}
<h3>Tab 1 content</h3>
{/if}
{#if 2 === currentTab}
<h3>Tab 2 content</h3>
{/if}
{#if 3 === currentTab}
<h3>Tab 3 content</h3>
{/if}
I’m also using currentTab
in if-block conditionals to determine what is the right content I want to display to the user.
Now, how in the world is the value of currentTab
being updated to display the right content?
Let’s look at the use of the <Tabs />
directive.
<Tabs bind:activeTabValue={currentTab} items={tabItems} />
When using the Tabs
directive, I’m passing the menu navigation down. But to update the currentTab
state property, I’m using the special keyword bind
.
bind:property
let’s you pass children component values (props or state) upward.
You can see that I’m telling Svelte to bind:property
the variable currentTab
with the component property activeTabValue
.
I do this by writing it as such, bind:activeTabValue={currentTab}
.
And if done correct, the output should look like this
I like to tweet about Svelte and post helpful code snippets. Follow me there if you would like some too!