Skip to content

Allow multiple classes in class: directive #7170

@Bastian

Description

@Bastian

Describe the problem

Utility-first CSS frameworks like Tailwind use very granular CSS classes (e.g. bg-red-500 for a red background, shadow-lg for a large box-shadow, ...). You often want to apply styles conditional with the class: directive. Unfortunately, it only works for a single class at the moment which means you have to duplicate it quite often. A very simple example for a Button component with Svelte and Tailwind might look like this:

<script lang="ts">
	export let color: 'primary' | 'danger' = 'primary';
</script>

<button
	on:click
	class="px-3 py-2 text-white rounded shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2"
	class:bg-blue-700={color === 'primary'}
	class:hover:bg-blue-800={color === 'primary'}
	class:ring-blue-400={color === 'primary'}
	class:bg-red-600={color === 'danger'}
	class:hover:bg-red-700={color === 'danger'}
	class:ring-red-500={color === 'danger'}
>
	<slot />
</button>

image

This is very boiler-plate-heavy and annoying to work with. It's also just a very simple example for showcasing and usually gets even uglier in real-world examples. When using a utility-first framework you run into this issue a lot.

Describe the proposed solution

Allow the use of multiple CSS classes in the class: directive with a class:"x y z"={true} syntax. This would allow the example above to be simplified like this:

<script lang="ts">
	export let color: 'primary' | 'danger' = 'primary';
</script>

<button
	on:click
	class="px-3 py-2 text-white rounded shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2"
	class:"bg-blue-700 hover:bg-blue-800 ring-blue-400"={color === 'primary'}
	class:"bg-red-600 hover:bg-red-700 ring-red-500"={color === 'danger'}
>
	<slot />
</button>

Alternatives considered

There's been a very similar issue (#3376) which unfortunately has been closed and not been re-opened despite getting a lot of follow-up comments that argue for its usefulness. In this issue, some alternatives have been discussed:

Using Tailwind's @apply directive

Tailwind does provide a @apply directive to extract multiple Tailwind-classes into a custom CSS class. For the example above, this could look like this:

<script lang="ts">
	export let color: 'primary' | 'danger' = 'primary';
</script>

<button
	on:click
	class="px-3 py-2 text-white rounded shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2"
	class:primary={color === 'primary'}
	class:danger={color === 'danger'}
>
	<slot />
</button>

<style lang="postcss">
	.danger {
		@apply bg-red-600 hover:bg-red-700 ring-red-500;
	}

	.primary {
		@apply bg-blue-700 hover:bg-blue-600 ring-blue-400;
	}
</style>

While this appears to be a good solution (and is used by many to circumvent the issue), using the @apply directive goes against the utility-first workflow. Adam Wathan (the creator of Tailwind) advised against using it (Source):

Confession: The apply feature in Tailwind basically only exists to trick people who are put off by long lists of classes into trying the framework.

You should almost never use it 😬

Additionally, there are other Utility-CSS frameworks that usually don't have this feature.

Using the ternary operator

<script lang="ts">
	export let color: 'primary' | 'danger' = 'primary';
</script>

<button
	on:click
	class="px-3 py-2 text-white rounded shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2
	{color === 'primary' ? 'bg-blue-700 hover:bg-blue-800 ring-blue-400' : ''}
	{color === 'danger' ? 'bg-red-600 hover:bg-red-700 ring-red-500' : ''}
	"
>
	<slot />
</button>

This does work, but obviously also introduces a lot of boilerplate code. The whole point of the class: directive is to eliminate this kind of code.

Writing a Svelte Preprocessor

I'm not familiar with preprocessors, but this has been a frequent suggestion in the original issue. There even exists one already: https://github.com/paulovieira/svelte-preprocess-class-directive

This might be a viable option but I would much rather prefer support out-of-the-box instead of relying on a third-party library. Besides not being actively maintained, the linked preprocessor uses an alternative, non-ideal syntax like described in the next section "Alternative syntaxes".

Alternative syntaxes

Many other syntaxes have been suggested, e.g. class:x,y,z={true), .x.y.z={true}, class:{"x y z"}={true}, ...
The problem with most of them is that they either introduce breaking changes (e.g., class:x,y,z={true} is already valid syntax for the class x,y,z) and/or limit it to a sub-set of CSS-classes because , and . are valid characters in CSS class names. While not very common in "classic" CSS classes, they are often used by utility frameworks like Tailwind (e.g. gap-[2.75rem], grid-rows-[200px_minmax(900px,_1fr)_100px], or row-[span_16_/_span_16]). class:{"x y z"}={true} would work and should be supported as an alternative syntax (just like class={"x y z"} also works) but is also unnecessary (yet small) boilerplate in most cases.

Importance

would make my life easier

Final words

As mentioned above, this is technically a duplicate of #3376. However, since there have been no responses from any maintainers (even when pinging them) on the original issue, I've decided to open this issue with a summary of the discussion in the original issue. I would very much appreciate a re-evaluation of the original decision to not support this feature, either in this issue or by re-opening the original one. Thank you for the awesome work on Svelte!

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions