Multiple Dynamic Classes in Svelte Using a Custom Directive

Imagine you have a custom Button component, and you want to dynamically apply styles based on a variant prop which can either be primary or secondary:

<script>
export let variant = 'primary';
</script>

<button 
    type="button"
    class="{`
        inline-flex items-center px-2.5 py-1.5
        ${variant === 'primary' ? ' text-white bg-indigo-600 ...' : ''} 
        ${variant === 'secondary' ? ' text-gray-700 bg-white ...' : ''}
    `}"
>
    <slot />
</button>

The problem

As you can see, using ternaries to concatenate the class string are fine but is not the most intuitive way of doing these kind of tasks.

Drawing inspiration from Vue.js

If you already used Vue, dynamically adding classes into the element is done by binding a class attribute with an object as value, in which values must be truthy and the keys must be the equivalent classes that will be applied, take a look:

<template>
    <button
        type="button"
        class="inline-flex items-center px-2.5 py-1.5 ..."
        :class="{
            'text-white bg-indigo-600 ...': variant === 'primary',
            'text-gray-700 bg-white ...': variant === 'secondary'
        }"
    >
        <slot />
    </button>
</template>

<script>
export default {
    props {
        variant: {
            type: String,
            default: 'primary'
        }
    }
}
</script>

Let's borrow it, but how?

Such syntax can be achieved using the help of Svelte's use:action.

Install the classnames library

To save up your time, we will just use the classnames library for dynamically adding class names. If you have experience using React.js, this might be a familiar library to you.

npm install --save classnames

Create the custom directive

In the root of the project, create a cx.js file, and put this:

import classnames from 'classnames';

export default function cx(node, value) {
    let originalClassList = [...node.classList];

    function mergeClass(newValue) {
        let classes = classnames(originalClassList, newValue)
            .split(' ')
            .filter(Boolean);

        if (classes.length > 0) {
            node.classList.add(...classes);
        }
    }

    function resetClass() {
        node.classList = originalClassList;
    }

    mergeClass(value);

    return {
        update(newValue) {
            resetClass();
            mergeClass(newValue);
        },

        destroy() {
            resetClass();
        },
    };
}

Finally, let's use it in the component

Instead of concatenating the classes, you just need to use:cx.

<script>
import cx from './cx';

export let variant = 'primary';
</script>

<button 
    type="button"
    class="inline-flex items-center px-2.5 py-1.5 ..."
    use:cx="{{
       'text-white bg-indigo-600 ...': variant === 'primary',
       'text-gray-700 bg-white ...': variant === 'secondary'
    }}"
>
    <slot />
</button>