Making Customizable Components in Svelte

Written on

I really like Svelte, but it has one big limitation in my opinion: it’s hard to make customizable, reusable components.

Let me demonstrate what I mean using a React example. In React, I would typically use one of the many ubiquitous UI libraries such as Chakra UI. Using this library, I’d write some code like this:

Svelte’s CSS selectors don’t apply to custom components, so you can’t just write CSS targeting Unless you want to add inline styles to your components —which have limitations of their own such as no media queries— you can’t really do something like this in Svelte though.

<script>
	import MyButton from './MyButton.svelte';
</script>

<MyButton>Hello!</MyButton>

<style>
	/* Does not work! */
	MyButton {
		background-color: #5e548e;
	}
</style>

I mean, that’s a bit misleading. You can do what UI libraries like Chakra do under the hood, which is using CSS-in-JS to dynamically generate your CSS at runtime. This comes at the cost of worse performance and more complexity though, so it’s not a silver bullet.

Using TailwindCSS though, you can get around this issue. TailwindCSS uses a compilation step that reads through the classes in your HTML (or svelte) files, and generates CSS to match the utility classes you used. This is great for us because TailwindCSS doesn’t really care if it’s a regular HTML component or a custom component where we write the classes. So you can do something like this:

<!-- In MyButton.svelte -->
<script>
	export let className = '';
</script>

<button class={className}><slot /></button>

<!-- In App.svelte -->
<script>
  import MyButton from './MyButton.svelte';
</script>

<!-- This does work! -->
<MyButton className="bg-indigo-900">Hello!</MyButton>

Okay great, but, my custom component basically does nothing right now. I haven’t actually customized anything beyond what a regular button is like. Can we do that? Yes! We just need to join the “default” classes together with the classes being passed in. TailwindCSS understands this.

<script lang="ts">export let className = "";
function clsx(...args) {
  return args.filter((arg) => !!arg).join(" ");
}
</script>

<!-- TailwindCSS understands this, and will generate the correct classes -->
<button class={clsx('text-white min-w-32 bg-blue-900', className)}><slot /></button>

I want to do one final improvement though. While we can do <button class="..."> for regular HTML components, our custom components have to use <MyButton className="...">. This is a bit annoying, because you have to switch between the two within your codebase. Thankfully, Svelte comes to our rescue here and lets us rename exported properties. We can do that like this:

let className = '';
export { className as class };

And then you’ll be able to do <MyButton class="..."> as well!

Let’s put all of this together in one example:

Combined with a TailwindCSS component library like DaisyUI, this makes for a really amazing developer experience that doesn’t sacrifice performance or maintainability.