Creating a Svelte Tabs component with Slot props

OpenReplay Tech Blog - Oct 11 '22 - - Dev Community

by Shinichi Okada

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

+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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

This is what we have created.

img2

View the Tabs component we have created so far.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

OpenReplay

Start enjoying your debugging experience - start using OpenReplay for free.

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

newsletter

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .