Back

Creating a Svelte Tabs component with Slot props

Creating a Svelte Tabs component with Slot props

When creating Svelte components, you may need to expose the parent component values to the child component. The Svelte let directive can do that through a slot.

In this article, we will create a Svelte tabs component using the let directive to expose a parent’s prop value to child elements and learn how to communicate the parent and child components.

The tabs component creates secondary navigation and toggles content inside a container.

Installation

We install SvelteKit and TailwindCSS.

npm create svelte@latest my-app
cd my-app
npm install
npx svelte-add@latest tailwindcss
npm i

Tabs basic structure

We are going to use the following structure:

<TabWrapper>
  <TabHead>
    <TabHeadItem>Tab 1</TabHeadItem>
    <TabHeadItem>Tab 2</TabHeadItem>
    <TabHeadItem>Tab 3</TabHeadItem>
  </TabHead>
  <TabContentItem>Tab 1 content</TabContentItem>
  <TabContentItem>Tab 2 content</TabContentItem>
  <TabContentItem>Tab 3 content</TabContentItem>
</TabWrapper>

TabWrapper holds other child components, and TabHead wraps head items with a ul tag. TabHeadItem holds a li tag and a on:click event forwarding. TabContentItem holds contents for each tab item.

TabWrapper

Our TabWrapper has a slot element to expose child components.

<script>
  export let divClass = 'w-full'
</script>

<div class={divClass}>
  <slot />
</div>

TabHead

We will fill up more CSS later for divClass. This component has a slot element to hold TabHeadItem.

<script>
  export let divClass = ''
  export let ulClass = 'flex flex-wrap -mb-px'
</script>

<div class={divClass}>
  <ul class={ulClass}  role="tablist">
    <slot />
  </ul>
</div>

TabHeadItem

This component has an on: directive with a click event to forward the event, and a slot element to hold a tab head name.

<script>
  export let id;
  export let buttonClass = ''
  export let liClass = 'mr-2'
</script>

<li class={liClass} role="presentation">
  <button
    on:click
    class={buttonClass}
    id="{id}-tabhead"
    type="button"
    role="tab">
    <slot />
  </button>
</li>

TabContentItem

We use an if statement to check the activeTabValue and id to show the tab content.

<script lang="ts">
  export let activeTabValue;
  export let id;
  export let contentDivClass = 'p-4 bg-gray-50 rounded-lg dark:bg-gray-300';
</script>

{#if activeTabValue === id}
  <div class={contentDivClass} id="{id}-tabitem" role="tabpanel" aria-labelledby="{id}-tab">
    <slot />
  </div>
{/if}

index.ts

We export all components in src/lib/index.ts.

export { default as TabWrapper } from './TabWrapper.svelte';
export { default as TabHead } from './TabHead.svelte';
export { default as TabHeadItem } from './TabHeadItem.svelte';
export { default as TabContentItem } from './TabContentItem.svelte';

+page

Let’s use the components we have created so far.

<script>
  import { TabWrapper, TabHead, TabHeadItem , TabContentItem } from '$lib'
  let activeTabValue = 1;
  const handleClick = (tabValue) => () => {
    activeTabValue = tabValue;
  };
</script>

<TabWrapper>
   <TabHead>
    <TabHeadItem id={1} on:click={handleClick(1)}>Tab 1</TabHeadItem>
    <TabHeadItem id={2} on:click={handleClick(2)}>Tab 2</TabHeadItem>
    <TabHeadItem id={3} on:click={handleClick(3)}>Tab 3</TabHeadItem>
  </TabHead>
  <TabContentItem id={1} {activeTabValue}>Tab 1 content</TabContentItem>
  <TabContentItem id={2} {activeTabValue}>Tab 2 content</TabContentItem>
  <TabContentItem id={3} {activeTabValue}>Tab 3 content</TabContentItem>
</TabWrapper>

We import all components from $lib and set an initial active tab using activeTabValue prop. The handleClick function handles a click event to set the activeTabValue to the TabContentItem’s id.
Each TabHeadItem holds a handleClick event with the corresponding id and each TabContentItem holds id and activeTabValue props.

img1

View the Tabs component in action

Highlighting an active tab head

Let’s add a highlight to an active tab head for the TabHeadItem component:

<script>
  // adding active class
  import classNames from 'classnames';
  export let id;
  export let activeTabValue
  export let inactiveClass = 'inline-block py-4 px-4 text-sm font-medium text-center text-gray-500 rounded-t-lg hover:text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300'
  export let activeClass = 'inline-block py-4 px-4 text-sm font-medium text-center text-blue-600 bg-gray-100 rounded-t-lg active dark:bg-gray-800 dark:text-blue-500'
  const liClass = 'mr-2'
</script>

<li class={liClass} role="presentation">
  <button
    on:click
    class={classNames(activeTabValue === id ? activeClass : inactiveClass)}
    id="{id}-tabhead"
    type="button"
    role="tab">
    <slot />
  </button>
</li>

Please run npm i -D classnames to install the classnames package. Once it is installed, we can import it as classNames.
We create the activeTabValue prop and new classes for active and inactive states.
We use classNames to change class using a conditional (ternary) operator to determine if the head item is active or inactive.

Let’s update the +page.svelte file:

<script>
  // updated version
  import { TabWrapper, TabHead, TabHeadItem , TabContentItem } from '$lib'
  let activeTabValue = 1;
  const handleClick = (tabValue) => () => {
    activeTabValue = tabValue;
  };
</script>

<TabWrapper>
   <TabHead>
    <TabHeadItem id={1} on:click={handleClick(1)} {activeTabValue}>Tab 1</TabHeadItem>
    <TabHeadItem id={2} on:click={handleClick(2)} {activeTabValue}>Tab 2</TabHeadItem>
    <TabHeadItem id={3} on:click={handleClick(3)} {activeTabValue}>Tab 3</TabHeadItem>
  </TabHead>
  <TabContentItem id={1} {activeTabValue}>Tab 1 content</TabContentItem>
  <TabContentItem id={2} {activeTabValue}>Tab 2 content</TabContentItem>
  <TabContentItem id={3} {activeTabValue}>Tab 3 content</TabContentItem>
</TabWrapper>

The only difference from the previous code is the {activeTabValue} prop in each TabHeadItem.

Let’s change the background style by adding CSS to src/app.html:

<body class="bg-gray-900">
  <div>%sveltekit.body%</div>
</body>

This is what we have created.

img2

View the Tabs component we have created so far.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

Adding different styles

In the last section, we will add one more style to our tabs component. We could use the following, but it is not optimal since it requires a bit of writing when you change the tabStyle prop.

We could do this way, but you have to change the style name in every TabHeadItem

<TabHead tabStyle='default'>
  <TabHeadItem id={1} tabStyle='default' {activeTabValue} on:click={handleClick(1)}>Profile</TabHeadItem>
  <TabHeadItem id={2} tabStyle='default' {activeTabValue} on:click={handleClick(2)}>Dashboard</TabHeadItem>
  <TabHeadItem id={3} tabStyle='default' {activeTabValue} on:click={handleClick(3)}>Settings</TabHeadItem>
  <TabHeadItem id={4} tabStyle='default' {activeTabValue} on:click={handleClick(4)}>Users</TabHeadItem>
</TabHead>

For our components, we are going to use a slot prop with the [let](https://svelte.dev/docs#template-syntax-slot-slot-key-value) directive. This allows us to expose a prop value to slot elements.

TabWrapper.svelte

This component set the tabStyle prop like <TabWrapper tabStyle='underline' let:tabStyle />.

<script lang='ts'>
  // example 3
  import classNames from 'classnames'
  export let divClass = 'w-full'
  export let tabStyle: 'default' | 'underline' ='default'
</script>

<div class={classNames(divClass, $$props.class)}>
  <slot {tabStyle} />
</div>

Import classNames and add the tabStyle prop. You can extend this by adding more style. We also add $$props.class so that we can use it likeclass="mb-8". We use {tabStyle} to expose the value to the slot elements.

TabHead.svelte

We also need to update the TabHead component:

<script lang='ts'>
  // example 3
  export let tabStyle: 'default' | 'underline' ='default'
  type classOptions = {
    [key: string]: string;
  }
  export const divClasses = {
    default: 'mb-4 border-b border-gray-200 dark:border-gray-700',
    underline: 'mb-4 text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700'
  }
  export const ulClasses = {
    default: 'flex flex-wrap -mb-px',
    underline: 'flex flex-wrap -mb-px'
  }
</script>

<div class={divClasses[tabStyle]}>
  <ul class={ulClasses[tabStyle]}  role="tablist">
    <slot />
   </ul>
</div>

Create the tabStyle same as the TabWrapper.svelte and add a type declaration using Typescript’s index signatures. We create objects that hold the style name as the key and CSS as the value and define a class depending on the tabStyle prop.

TabHeadItem.svelte

<script lang='ts'>
  // example 3
  import classNames from 'classnames';
  export let id;
  export let activeTabValue
  type classOptions = {
    [key: string]: string;
  }
  const activeClasses: classOptions  = {
    default: 'inline-block py-4 px-4 text-sm font-medium text-center text-blue-600 bg-gray-100 rounded-t-lg active dark:bg-gray-800 dark:text-blue-500', 
    underline: 'inline-block p-4 text-blue-600 rounded-t-lg border-b-2 border-blue-600 active dark:text-blue-500 dark:border-blue-500'
  }
  const inactiveClasses: classOptions  = {
    default: 'inline-block py-4 px-4 text-sm font-medium text-center text-gray-500 rounded-t-lg hover:text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300',
    underline: 'inline-block p-4 rounded-t-lg border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300'
  }
  const liClasses : classOptions = {
    default: 'mr-2',
    underline: 'mr-2'
  };
  export let tabStyle: 'default' | 'underline' ='default'
</script>

<li class={liClasses[tabStyle]} role="presentation">
  <button
    on:click
    class={classNames(activeTabValue === id ? activeClasses[tabStyle] : inactiveClasses[tabStyle])}
    id="{id}-tabhead"
    type="button"
    role="tab">
    <slot />
  </button>
</li>

We create objects activeClasses, inactiveClasses, and liClasses that hold the style name as the key and CSS as the value. We also define a class depending on the tabStyle prop, class={liClasses[tabStyle]} and class={classNames(activeTabValue === id ? activeClasses[tabStyle] : inactiveClasses[tabStyle])}.

<script>
  // example 3
  import  { TabWrapper, TabHead, TabHeadItem , TabContentItem } from '$lib'
  let activeTabValue = 1;
  let activeTabValue2 = 1;
  const handleClick = (tabValue) => () => {
    activeTabValue = tabValue;
  };
  const handleClick2 = (tabValue) => () => {
    activeTabValue2 = tabValue;
  };
</script>

<TabWrapper class="mb-8">
  <TabHead>
    <TabHeadItem id={1}  on:click={handleClick(1)} {activeTabValue}>Tab 1</TabHeadItem>
    <TabHeadItem id={2} on:click={handleClick(2)} {activeTabValue}>Tab 2</TabHeadItem>
    <TabHeadItem id={3} on:click={handleClick(3)} {activeTabValue}>Tab 3</TabHeadItem>
  </TabHead>
  <TabContentItem id={1} {activeTabValue}>Tab 1 content</TabContentItem>
  <TabContentItem id={2} {activeTabValue}>Tab 2 content</TabContentItem>
  <TabContentItem id={3} {activeTabValue}>Tab 3 content</TabContentItem>
</TabWrapper>

<TabWrapper tabStyle='underline' let:tabStyle>
  <TabHead {tabStyle}>
    <TabHeadItem id={1} {tabStyle} on:click={handleClick2(1)} activeTabValue={activeTabValue2}>Tab 1</TabHeadItem>
    <TabHeadItem id={2} {tabStyle} on:click={handleClick2(2)} activeTabValue={activeTabValue2}>Tab 2</TabHeadItem>
    <TabHeadItem id={3} {tabStyle} on:click={handleClick2(3)} activeTabValue={activeTabValue2}>Tab 3</TabHeadItem>
  </TabHead>
  <TabContentItem id={1} activeTabValue={activeTabValue2}>Tab 1 content</TabContentItem>
  <TabContentItem id={2} activeTabValue={activeTabValue2}>Tab 2 content</TabContentItem>
  <TabContentItem id={3} activeTabValue={activeTabValue2}>Tab 3 content</TabContentItem>
</TabWrapper>

We are going to add two examples. Create two prop, activeTabValue and activeTabValue2 and event functions, handleClick and handleClick2.
The first example is the default style tabs example, and the second is the underline style tabs example. In line 25, the directive value must be a JavaScript expression enclosed in curly braces. This means you can’t use let:tabStyle='underline.
To get the tabStyle value from the TabHead component, use {tabStyle} in child components.

This is our final result.

img3

View the final Tabs component we have created.

Conclusion

To extend this component, you can add different event handlers, such as on:mouseenter, on:mouseleave, on:keydown, etc., to the TabHeadItem component. Also, you can add more styles.

I hope these examples showed how to expose a parent prop value to slot elements using a slot prop with the let directive.

Flowbite-Svelte

(Disclaimer: I’m a contributor to Flowbite-Svelte, an open-source project.)

Flowbite-Svelte is an official Flowbite component library for Svelte. We used similar component structures and added more styles and functions. Flowbite-Svelte’s Tabs component styles are tabs with underline, tabs with icons, pills tabs, full-width tabs, and more.

4

Default tabs

5

Tabs with underline example

6

Tabs with icons example

7

Pills tabs example

8

A TIP FROM THE EDITOR: Learn more about Svelte in our A Practical Introduction To Svelte article.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs and track user frustrations. Get complete visibility into your frontend with OpenReplay, the most advanced open-source session replay tool for developers.

OpenReplay