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>