Skip to main content
Svelte Fundamentals
CHAPTER 25 Beginner

Building Reusable UI Components

Updated: May 18, 2026
5 min read

# CHAPTER 25

Building Reusable UI Components

1. Chapter Introduction

Professional teams build component libraries — collections of consistently designed, thoroughly tested, reusable UI primitives. In Svelte, this is more enjoyable than any other framework because of scoped styles, slot composition, and clean prop APIs. This chapter builds a production-ready mini UI kit.

2. Learning Objectives

  • Build a <Button> component with variants and sizes.
  • Build a reusable <Modal> component.
  • Build an <Input> component with error state.
  • Build a <Badge>, <Card>, and <Toast> notification system.
  • Follow design system principles.

3. Button Component

svelte
123456789101112131415161718192021222324252627282930313233343536373839
<!-- src/lib/ui/Button.svelte -->
<script>
  export let variant = &#039;primary'; // primary | secondary | danger | ghost
  export let size = &#039;md';         // sm | md | lg
  export let disabled = false;
  export let loading = false;
  export let type = &#039;button';

  const variantClasses = {
    primary: &#039;bg-indigo-600 hover:bg-indigo-700 text-white',
    secondary: &#039;bg-gray-100 hover:bg-gray-200 text-gray-800',
    danger: &#039;bg-red-500 hover:bg-red-600 text-white',
    ghost: &#039;hover:bg-gray-100 text-gray-700'
  };

  const sizeClasses = {
    sm: &#039;px-3 py-1.5 text-sm',
    md: &#039;px-4 py-2',
    lg: &#039;px-6 py-3 text-lg'
  };
</script>

<button
  {type}
  {disabled}
  class="inline-flex items-center gap-2 font-medium rounded-lg transition-colors
    {variantClasses[variant]} {sizeClasses[size]}
    {disabled ? &#039;opacity-50 cursor-not-allowed' : ''}"
  on:click
  {...$$restProps}
>
  {#if loading}
    <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
      <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
    </svg>
  {/if}
  <slot />
</button>

4. Input Component

svelte
123456789101112131415161718192021222324252627282930313233343536
<!-- src/lib/ui/Input.svelte -->
<script>
  export let label = &#039;';
  export let value = &#039;';
  export let error = &#039;';
  export let hint = &#039;';
  export let type = &#039;text';
  export let placeholder = &#039;';
  export let required = false;
</script>

<div class="input-wrapper">
  {#if label}
    <label class="block text-sm font-medium text-gray-700 mb-1">
      {label} {#if required}<span class="text-red-500">*</span>{/if}
    </label>
  {/if}

  <input
    {type}
    {placeholder}
    {required}
    bind:value
    class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 transition-colors
      {error ? &#039;border-red-400 focus:ring-red-300' : 'border-gray-300 focus:ring-indigo-400'}"
    {...$$restProps}
    on:input
    on:blur
  />

  {#if error}
    <p class="text-red-500 text-sm mt-1">⚠️ {error}</p>
  {:else if hint}
    <p class="text-gray-500 text-sm mt-1">{hint}</p>
  {/if}
</div>

5. Modal Component

svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445
<!-- src/lib/ui/Modal.svelte -->
<script>
  import { fly, fade } from &#039;svelte/transition';
  export let open = false;
  export let title = &#039;';
  export let size = &#039;md'; // sm | md | lg | xl

  const sizeMap = { sm: &#039;max-w-sm', md: 'max-w-md', lg: 'max-w-lg', xl: 'max-w-xl' };

  function close() { open = false; }

  function handleKeydown(e) {
    if (e.key === &#039;Escape') close();
  }
</script>

<svelte:window on:keydown={handleKeydown} />

{#if open}
  <div class="fixed inset-0 z-50 flex items-center justify-center p-4" transition:fade={{ duration: 150 }}>
    <!-- Backdrop -->
    <div class="absolute inset-0 bg-black/50" on:click={close} />

    <!-- Dialog -->
    <div
      class="relative bg-white rounded-2xl shadow-2xl w-full {sizeMap[size]}"
      transition:fly={{ y: -20, duration: 200 }}
    >
      {#if title}
        <div class="flex items-center justify-between p-6 border-b">
          <h2 class="text-xl font-semibold">{title}</h2>
          <button class="text-gray-400 hover:text-gray-600 text-2xl leading-none" on:click={close}>✕</button>
        </div>
      {/if}

      <div class="p-6"><slot /></div>

      {#if $$slots.footer}
        <div class="flex items-center justify-end gap-3 p-6 border-t bg-gray-50 rounded-b-2xl">
          <slot name="footer" />
        </div>
      {/if}
    </div>
  </div>
{/if}

6. Toast Notification System

javascript
1234567891011121314
// src/lib/stores/toastStore.js
import { writable } from &#039;svelte/store&#039;;

export const toasts = writable([]);

export function toast(message, type = &#039;info&#039;, duration = 3000) {
  const id = Date.now();
  toasts.update(all => [...all, { id, message, type }]);
  setTimeout(() => toasts.update(all => all.filter(t => t.id !== id)), duration);
}

export const success = (msg) => toast(msg, &#039;success&#039;);
export const error = (msg) => toast(msg, &#039;error&#039;);
export const info = (msg) => toast(msg, &#039;info&#039;);
svelte
123456789101112131415161718192021222324
<!-- src/lib/ui/Toaster.svelte — Place in App.svelte -->
<script>
  import { fly } from &#039;svelte/transition';
  import { flip } from &#039;svelte/animate';
  import { toasts } from &#039;$lib/stores/toastStore.js';

  const typeClasses = {
    success: &#039;bg-green-500', error: 'bg-red-500', info: 'bg-indigo-500'
  };
  const typeIcons = { success: &#039;✅', error: '❌', info: 'ℹ️' };
</script>

<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
  {#each $toasts as toast (toast.id)}
    <div
      class="flex items-center gap-3 text-white px-4 py-3 rounded-xl shadow-lg {typeClasses[toast.type]}"
      animate:flip={{ duration: 200 }}
      transition:fly={{ x: 100, duration: 300 }}
    >
      <span>{typeIcons[toast.type]}</span>
      <span>{toast.message}</span>
    </div>
  {/each}
</div>

7. Badge Component

svelte
12345678910111213141516
<!-- Badge.svelte -->
<script>
  export let variant = &#039;gray'; // gray | green | red | yellow | blue | purple
  const styles = {
    gray: &#039;bg-gray-100 text-gray-700',
    green: &#039;bg-green-100 text-green-700',
    red: &#039;bg-red-100 text-red-700',
    yellow: &#039;bg-yellow-100 text-yellow-700',
    blue: &#039;bg-blue-100 text-blue-700',
    purple: &#039;bg-purple-100 text-purple-700'
  };
</script>

<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {styles[variant]}">
  <slot />
</span>

8. MCQs

Question 1

What does {...$$restProps} do in a wrapper component?

Question 2

What does on:click (without a handler) in a component do?

Question 3

How do you detect if a named slot has content to conditionally render its container?

Question 4

What Svelte transition is best for modals (fade backdrop + slide dialog)?

Question 5

How do you close a modal by pressing Escape key?

Question 6

What is a design token?

Question 7

Why use variant prop for Button styles instead of passing classes directly?

Question 8

What Svelte animate directive creates smooth toast list reordering?

Question 9

What is $$slots in Svelte?

Question 10

What makes a component truly reusable?

9. Interview Questions

  • Q: What principles make a component library maintainable over time?
  • Q: How would you build a toast notification system that any component can trigger?

10. Summary

Building a component library in Svelte is a rewarding exercise in component API design. Slots provide composition. $$restProps provides HTML attribute forwarding. $$slots enables conditional rendering of slot containers. The result is a set of primitive components that form the visual foundation of any application.

11. Next Chapter Recommendation

In Chapter 26: Svelte Interview Preparation, we compile 50 interview questions covering reactivity, stores, SvelteKit, performance, and coding challenges to prepare you for any Svelte technical interview.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·