Svelte

Web framework with compile-time optimization. Compiles to vanilla JavaScript at build time, achieving high performance without runtime virtual DOM.

JavaScriptFrameworkCompilerReactivePerformanceComponentUI

GitHub Overview

sveltejs/svelte

web development for the rest of us

Stars83,714
Watchers852
Forks4,591
Created:November 20, 2016
Language:JavaScript
License:MIT License

Topics

compilertemplateui

Star History

sveltejs/svelte Star History
Data as of: 8/13/2025, 01:43 AM

Framework

Svelte

Overview

Svelte is an innovative web component framework that generates high-performance JavaScript code optimized at compile time. It requires no Virtual DOM or runtime overhead.

Details

Svelte is a web application framework developed by Rich Harris in 2016, taking a fundamentally different approach from traditional frameworks like React and Vue.js. Svelte is a compiler framework that transforms components into optimized vanilla JavaScript at build time, eliminating the need for Virtual DOM or runtime frameworks. Key features include concise and intuitive syntax, reactive state management, component-scoped CSS, built-in animation and transition support, small bundle sizes, high performance, TypeScript support, and built-in accessibility support. Svelte 5 introduces the Runes system with new reactivity features like $state, $derived, $effect, and $props. It's particularly adopted for performance-critical web applications, interactive dashboards, data visualization, and mobile application development, used by major companies including The New York Times, Apple, Spotify, and IKEA.

Pros and Cons

Pros

  • Compile-time Optimization: Fast execution without Virtual DOM
  • Small Bundle Size: Near-zero runtime overhead
  • Concise Syntax: Intuitive and easy-to-learn syntax
  • Reactive State Management: No need for complex state management libraries
  • Component-scoped CSS: Automatic CSS scope management
  • Built-in Animations: Easy implementation of beautiful transitions
  • TypeScript Support: Excellent type inference and developer experience
  • Accessibility: Built-in a11y checks and warnings

Cons

  • Small Ecosystem: Fewer libraries compared to React/Vue.js
  • Compilation Step: Build process required
  • Debug Difficulty: Challenging to debug compiled code
  • Community Size: Smaller compared to other major frameworks
  • Server-side Rendering: Primarily a client-side framework
  • Learning Resources: Limited Japanese language resources
  • Migration Cost: Cost of migrating from other frameworks

Key Links

Code Examples

Hello World

<!-- App.svelte -->
<script>
  // JavaScript logic
  let name = 'Svelte';
  let count = 0;
  
  // Reactive statements
  $: doubled = count * 2;
  $: if (count >= 10) {
    alert('Count reached 10!');
  }
  
  // Event handlers
  function increment() {
    count += 1;
  }
  
  function decrement() {
    count -= 1;
  }
  
  function reset() {
    count = 0;
  }
</script>

<!-- HTML template -->
<main>
  <h1>Hello {name}!</h1>
  
  <div class="counter">
    <p>Count: {count}</p>
    <p>Doubled: {doubled}</p>
    
    <div class="buttons">
      <button on:click={decrement}>-</button>
      <button on:click={reset}>Reset</button>
      <button on:click={increment}>+</button>
    </div>
  </div>
  
  <!-- Conditional rendering -->
  {#if count > 5}
    <p class="warning">Count is high!</p>
  {:else if count < 0}
    <p class="error">Count is negative!</p>
  {/if}
</main>

<!-- Component-scoped CSS -->
<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  .counter {
    margin: 2em 0;
    padding: 1em;
    border: 1px solid #ddd;
    border-radius: 8px;
  }

  .buttons {
    display: flex;
    gap: 0.5em;
    justify-content: center;
    margin-top: 1em;
  }

  button {
    background: #4CAF50;
    color: white;
    border: none;
    padding: 0.5em 1em;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1em;
  }

  button:hover {
    background: #45a049;
  }

  .warning {
    color: #ff9800;
    font-weight: bold;
  }

  .error {
    color: #f44336;
    font-weight: bold;
  }

  h1 {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

Components and Props

<!-- UserCard.svelte -->
<script>
  // Props definition
  export let user;
  export let showActions = true;
  export let theme = 'light';
  
  // Event dispatchers
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();
  
  // Computed properties
  $: initials = user.name
    .split(' ')
    .map(n => n[0])
    .join('')
    .toUpperCase();
  
  $: isActive = user.lastSeen && 
    new Date() - new Date(user.lastSeen) < 5 * 60 * 1000; // 5 minutes
  
  // Event handlers
  function handleEdit() {
    dispatch('edit', user);
  }
  
  function handleDelete() {
    dispatch('delete', user.id);
  }
  
  function handleMessage() {
    dispatch('message', { userId: user.id, userName: user.name });
  }
</script>

<div class="user-card {theme}" class:active={isActive}>
  <div class="avatar">
    {#if user.avatar}
      <img src={user.avatar} alt="{user.name}'s avatar" />
    {:else}
      <div class="initials">{initials}</div>
    {/if}
    {#if isActive}
      <div class="status-indicator" title="Online"></div>
    {/if}
  </div>
  
  <div class="user-info">
    <h3>{user.name}</h3>
    <p class="email">{user.email}</p>
    <p class="role">{user.role}</p>
    {#if user.bio}
      <p class="bio">{user.bio}</p>
    {/if}
  </div>
  
  {#if showActions}
    <div class="actions">
      <button on:click={handleMessage} class="btn btn-primary">
        Message
      </button>
      <button on:click={handleEdit} class="btn btn-secondary">
        Edit
      </button>
      <button on:click={handleDelete} class="btn btn-danger">
        Delete
      </button>
    </div>
  {/if}
</div>

<style>
  .user-card {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    background: white;
    transition: all 0.2s ease;
  }

  .user-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
  }

  .user-card.dark {
    background: #2d3748;
    border-color: #4a5568;
    color: white;
  }

  .user-card.active {
    border-color: #4CAF50;
  }

  .avatar {
    position: relative;
    width: 60px;
    height: 60px;
    border-radius: 50%;
    overflow: hidden;
  }

  .avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .initials {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #4CAF50;
    color: white;
    font-weight: bold;
    font-size: 1.2rem;
  }

  .status-indicator {
    position: absolute;
    bottom: 2px;
    right: 2px;
    width: 12px;
    height: 12px;
    background: #4CAF50;
    border: 2px solid white;
    border-radius: 50%;
  }

  .user-info {
    flex: 1;
  }

  .user-info h3 {
    margin: 0 0 0.5rem 0;
    font-size: 1.2rem;
  }

  .email {
    color: #666;
    margin: 0;
  }

  .role {
    background: #e3f2fd;
    color: #1976d2;
    padding: 0.25rem 0.5rem;
    border-radius: 12px;
    font-size: 0.875rem;
    display: inline-block;
    margin: 0.5rem 0;
  }

  .bio {
    font-size: 0.875rem;
    color: #888;
    margin: 0.5rem 0 0 0;
  }

  .actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  .btn {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.875rem;
    transition: background 0.2s;
  }

  .btn-primary {
    background: #2196F3;
    color: white;
  }

  .btn-secondary {
    background: #f0f0f0;
    color: #333;
  }

  .btn-danger {
    background: #f44336;
    color: white;
  }

  .btn:hover {
    opacity: 0.8;
  }
</style>
<!-- UserList.svelte -->
<script>
  import UserCard from './UserCard.svelte';
  
  export let users = [];
  export let loading = false;
  export let theme = 'light';
  
  // Event handlers
  function handleEditUser(event) {
    console.log('Edit user:', event.detail);
    // Implement edit logic
  }
  
  function handleDeleteUser(event) {
    console.log('Delete user:', event.detail);
    // Implement delete logic
  }
  
  function handleMessageUser(event) {
    console.log('Message user:', event.detail);
    // Implement messaging logic
  }
</script>

<div class="user-list">
  {#if loading}
    <div class="loading">
      <div class="spinner"></div>
      <p>Loading users...</p>
    </div>
  {:else if users.length === 0}
    <div class="empty-state">
      <h3>No users found</h3>
      <p>Try adjusting your search criteria.</p>
    </div>
  {:else}
    <div class="users-grid">
      {#each users as user (user.id)}
        <UserCard
          {user}
          {theme}
          on:edit={handleEditUser}
          on:delete={handleDeleteUser}
          on:message={handleMessageUser}
        />
      {/each}
    </div>
  {/if}
</div>

<style>
  .user-list {
    width: 100%;
  }

  .loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 3rem;
  }

  .spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid #4CAF50;
    border-radius: 50%;
    animation: spin 1s linear infinite;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }

  .empty-state {
    text-align: center;
    padding: 3rem;
    color: #666;
  }

  .users-grid {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
  }

  @media (max-width: 768px) {
    .users-grid {
      grid-template-columns: 1fr;
    }
  }
</style>

Store Management

// stores/userStore.js
import { writable, derived } from 'svelte/store';

// User store
function createUserStore() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,
    // Load users from API
    loadUsers: async () => {
      try {
        const response = await fetch('/api/users');
        const users = await response.json();
        set(users);
      } catch (error) {
        console.error('Error loading users:', error);
      }
    },
    // Add new user
    addUser: (user) => update(users => [...users, { ...user, id: Date.now() }]),
    // Update user
    updateUser: (id, updatedUser) => update(users => 
      users.map(user => user.id === id ? { ...user, ...updatedUser } : user)
    ),
    // Remove user
    removeUser: (id) => update(users => users.filter(user => user.id !== id)),
    // Clear all users
    clear: () => set([])
  };
}

export const userStore = createUserStore();

// Authentication store
function createAuthStore() {
  const { subscribe, set, update } = writable({
    user: null,
    isAuthenticated: false,
    loading: false
  });

  return {
    subscribe,
    // Login
    login: async (credentials) => {
      update(state => ({ ...state, loading: true }));
      try {
        const response = await fetch('/api/auth/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credentials)
        });
        
        if (response.ok) {
          const user = await response.json();
          set({ user, isAuthenticated: true, loading: false });
          return { success: true };
        } else {
          throw new Error('Login failed');
        }
      } catch (error) {
        update(state => ({ ...state, loading: false }));
        return { success: false, error: error.message };
      }
    },
    // Logout
    logout: () => {
      set({ user: null, isAuthenticated: false, loading: false });
      // Clear any stored tokens
      localStorage.removeItem('auth-token');
    },
    // Initialize from stored session
    initialize: () => {
      const token = localStorage.getItem('auth-token');
      if (token) {
        // Verify token and set user state
        // Implementation depends on your auth system
      }
    }
  };
}

export const authStore = createAuthStore();

// Derived stores
export const authenticatedUser = derived(
  authStore,
  $auth => $auth.isAuthenticated ? $auth.user : null
);

export const userCount = derived(
  userStore,
  $users => $users.length
);

export const activeUsers = derived(
  userStore,
  $users => $users.filter(user => user.status === 'active')
);

// Theme store
export const theme = writable('light');

// Search store
function createSearchStore() {
  const { subscribe, set } = writable({
    query: '',
    filters: {},
    results: []
  });

  return {
    subscribe,
    setQuery: (query) => update(state => ({ ...state, query })),
    setFilters: (filters) => update(state => ({ ...state, filters })),
    search: async (query, filters = {}) => {
      update(state => ({ ...state, query, filters }));
      
      try {
        const searchParams = new URLSearchParams({
          q: query,
          ...filters
        });
        
        const response = await fetch(`/api/search?${searchParams}`);
        const results = await response.json();
        
        update(state => ({ ...state, results }));
      } catch (error) {
        console.error('Search error:', error);
      }
    },
    clear: () => set({ query: '', filters: {}, results: [] })
  };
}

export const searchStore = createSearchStore();
<!-- Using stores in components -->
<script>
  import { userStore, authStore, theme } from './stores/userStore.js';
  import { onMount } from 'svelte';
  
  // Subscribe to stores
  $: users = $userStore;
  $: auth = $authStore;
  $: currentTheme = $theme;
  
  // Load users on component mount
  onMount(() => {
    userStore.loadUsers();
    authStore.initialize();
  });
  
  // Add new user
  function addUser() {
    const newUser = {
      name: 'New User',
      email: '[email protected]',
      role: 'user'
    };
    userStore.addUser(newUser);
  }
  
  // Toggle theme
  function toggleTheme() {
    theme.update(t => t === 'light' ? 'dark' : 'light');
  }
</script>

<div class="app" class:dark={currentTheme === 'dark'}>
  <header>
    <h1>User Management</h1>
    <button on:click={toggleTheme}>
      Toggle {currentTheme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  </header>
  
  {#if auth.isAuthenticated}
    <main>
      <button on:click={addUser}>Add User</button>
      <p>Total users: {users.length}</p>
      
      {#each users as user (user.id)}
        <div class="user-item">
          {user.name} - {user.email}
        </div>
      {/each}
    </main>
  {:else}
    <div class="login-required">
      Please log in to view users.
    </div>
  {/if}
</div>

Animations and Transitions

<script>
  import { fade, fly, slide, scale } from 'svelte/transition';
  import { flip } from 'svelte/animate';
  import { quintOut } from 'svelte/easing';
  
  let items = [
    { id: 1, text: 'Item 1', color: '#ff6b6b' },
    { id: 2, text: 'Item 2', color: '#4ecdc4' },
    { id: 3, text: 'Item 3', color: '#45b7d1' },
    { id: 4, text: 'Item 4', color: '#96ceb4' },
    { id: 5, text: 'Item 5', color: '#ffeaa7' }
  ];
  
  let showList = true;
  let selectedItem = null;
  
  function addItem() {
    const newId = Math.max(...items.map(i => i.id)) + 1;
    const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'];
    const newItem = {
      id: newId,
      text: `Item ${newId}`,
      color: colors[Math.floor(Math.random() * colors.length)]
    };
    items = [...items, newItem];
  }
  
  function removeItem(id) {
    items = items.filter(item => item.id !== id);
  }
  
  function shuffleItems() {
    items = items.sort(() => Math.random() - 0.5);
  }
  
  function selectItem(item) {
    selectedItem = selectedItem?.id === item.id ? null : item;
  }
</script>

<div class="animation-demo">
  <div class="controls">
    <button on:click={() => showList = !showList}>
      {showList ? 'Hide' : 'Show'} List
    </button>
    <button on:click={addItem}>Add Item</button>
    <button on:click={shuffleItems}>Shuffle</button>
  </div>

  {#if showList}
    <div class="item-list" transition:slide={{ duration: 300 }}>
      {#each items as item (item.id)}
        <div
          class="item"
          style="background-color: {item.color}"
          animate:flip={{ duration: 300 }}
          transition:fly={{ y: 20, duration: 300 }}
          on:click={() => selectItem(item)}
          class:selected={selectedItem?.id === item.id}
        >
          <span>{item.text}</span>
          <button
            class="remove-btn"
            on:click|stopPropagation={() => removeItem(item.id)}
            transition:scale={{ duration: 200 }}
          >
            ×
          </button>
        </div>
      {/each}
    </div>
  {/if}

  {#if selectedItem}
    <div
      class="item-details"
      transition:fade={{ duration: 200 }}
    >
      <h3>Selected Item</h3>
      <p>ID: {selectedItem.id}</p>
      <p>Text: {selectedItem.text}</p>
      <p>Color: {selectedItem.color}</p>
      <button on:click={() => selectedItem = null}>Close</button>
    </div>
  {/if}
</div>

<style>
  .animation-demo {
    max-width: 600px;
    margin: 0 auto;
    padding: 2rem;
  }

  .controls {
    display: flex;
    gap: 1rem;
    margin-bottom: 2rem;
  }

  .controls button {
    padding: 0.5rem 1rem;
    background: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .controls button:hover {
    background: #45a049;
  }

  .item-list {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  .item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    border-radius: 8px;
    color: white;
    font-weight: bold;
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
  }

  .item:hover {
    transform: translateX(5px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  }

  .item.selected {
    transform: scale(1.05);
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
  }

  .remove-btn {
    background: rgba(255, 255, 255, 0.3);
    color: white;
    border: none;
    border-radius: 50%;
    width: 30px;
    height: 30px;
    cursor: pointer;
    font-size: 1.2rem;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .remove-btn:hover {
    background: rgba(255, 255, 255, 0.5);
  }

  .item-details {
    margin-top: 2rem;
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
    background: #f9f9f9;
  }

  .item-details h3 {
    margin-top: 0;
  }

  .item-details button {
    padding: 0.5rem 1rem;
    background: #f44336;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 1rem;
  }
</style>

HTTP Requests

<script>
  import { onMount } from 'svelte';
  
  // API configuration
  const API_BASE = 'https://jsonplaceholder.typicode.com';
  
  // State variables
  let posts = [];
  let users = [];
  let selectedUser = null;
  let loading = false;
  let error = null;
  
  // New post form
  let newPost = {
    title: '',
    body: '',
    userId: 1
  };
  
  // Generic HTTP request function
  async function apiRequest(endpoint, options = {}) {
    const url = `${API_BASE}${endpoint}`;
    const config = {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    };
    
    const response = await fetch(url, config);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return response.json();
  }
  
  // Load posts
  async function loadPosts() {
    try {
      loading = true;
      error = null;
      posts = await apiRequest('/posts');
    } catch (err) {
      error = 'Failed to load posts: ' + err.message;
    } finally {
      loading = false;
    }
  }
  
  // Load users
  async function loadUsers() {
    try {
      users = await apiRequest('/users');
    } catch (err) {
      console.error('Failed to load users:', err);
    }
  }
  
  // Load posts by user
  async function loadPostsByUser(userId) {
    try {
      loading = true;
      error = null;
      posts = await apiRequest(`/posts?userId=${userId}`);
      selectedUser = users.find(u => u.id === userId);
    } catch (err) {
      error = 'Failed to load user posts: ' + err.message;
    } finally {
      loading = false;
    }
  }
  
  // Create new post
  async function createPost() {
    if (!newPost.title.trim() || !newPost.body.trim()) {
      alert('Please fill in all fields');
      return;
    }
    
    try {
      const createdPost = await apiRequest('/posts', {
        method: 'POST',
        body: JSON.stringify(newPost)
      });
      
      // Add to local posts array
      posts = [createdPost, ...posts];
      
      // Reset form
      newPost = { title: '', body: '', userId: 1 };
      
      alert('Post created successfully!');
    } catch (err) {
      error = 'Failed to create post: ' + err.message;
    }
  }
  
  // Update post
  async function updatePost(postId, updatedData) {
    try {
      const updatedPost = await apiRequest(`/posts/${postId}`, {
        method: 'PUT',
        body: JSON.stringify(updatedData)
      });
      
      // Update local posts array
      posts = posts.map(post => 
        post.id === postId ? { ...post, ...updatedPost } : post
      );
      
      alert('Post updated successfully!');
    } catch (err) {
      error = 'Failed to update post: ' + err.message;
    }
  }
  
  // Delete post
  async function deletePost(postId) {
    if (!confirm('Are you sure you want to delete this post?')) {
      return;
    }
    
    try {
      await apiRequest(`/posts/${postId}`, {
        method: 'DELETE'
      });
      
      // Remove from local posts array
      posts = posts.filter(post => post.id !== postId);
      
      alert('Post deleted successfully!');
    } catch (err) {
      error = 'Failed to delete post: ' + err.message;
    }
  }
  
  // Load data on component mount
  onMount(() => {
    loadPosts();
    loadUsers();
  });
</script>

<div class="http-demo">
  <header>
    <h1>Posts Manager</h1>
    <p>Demonstrating HTTP requests with Svelte</p>
  </header>

  <!-- User filter -->
  <div class="user-filter">
    <label for="user-select">Filter by user:</label>
    <select id="user-select" on:change={(e) => {
      const userId = parseInt(e.target.value);
      if (userId) {
        loadPostsByUser(userId);
      } else {
        loadPosts();
        selectedUser = null;
      }
    }}>
      <option value="">All users</option>
      {#each users as user}
        <option value={user.id}>{user.name}</option>
      {/each}
    </select>
    
    {#if selectedUser}
      <div class="selected-user">
        Showing posts by: <strong>{selectedUser.name}</strong>
        <button on:click={() => {
          loadPosts();
          selectedUser = null;
        }}>Clear filter</button>
      </div>
    {/if}
  </div>

  <!-- Create new post form -->
  <div class="create-post">
    <h3>Create New Post</h3>
    <form on:submit|preventDefault={createPost}>
      <div class="form-group">
        <label for="title">Title:</label>
        <input
          id="title"
          type="text"
          bind:value={newPost.title}
          placeholder="Enter post title"
          required
        />
      </div>
      
      <div class="form-group">
        <label for="body">Content:</label>
        <textarea
          id="body"
          bind:value={newPost.body}
          placeholder="Enter post content"
          rows="4"
          required
        ></textarea>
      </div>
      
      <div class="form-group">
        <label for="user-id">User:</label>
        <select id="user-id" bind:value={newPost.userId}>
          {#each users as user}
            <option value={user.id}>{user.name}</option>
          {/each}
        </select>
      </div>
      
      <button type="submit">Create Post</button>
    </form>
  </div>

  <!-- Error display -->
  {#if error}
    <div class="error">
      {error}
      <button on:click={() => error = null}>×</button>
    </div>
  {/if}

  <!-- Loading indicator -->
  {#if loading}
    <div class="loading">
      <div class="spinner"></div>
      <p>Loading posts...</p>
    </div>
  {/if}

  <!-- Posts list -->
  <div class="posts-list">
    <h3>Posts ({posts.length})</h3>
    
    {#if posts.length === 0 && !loading}
      <p class="no-posts">No posts found.</p>
    {:else}
      {#each posts as post (post.id)}
        <article class="post">
          <h4>{post.title}</h4>
          <p>{post.body}</p>
          <div class="post-meta">
            <span>Post ID: {post.id}</span>
            <span>User ID: {post.userId}</span>
          </div>
          <div class="post-actions">
            <button 
              on:click={() => updatePost(post.id, { 
                title: post.title + ' (Updated)',
                body: post.body + ' [Updated content]'
              })}
              class="btn-update"
            >
              Update
            </button>
            <button 
              on:click={() => deletePost(post.id)}
              class="btn-delete"
            >
              Delete
            </button>
          </div>
        </article>
      {/each}
    {/if}
  </div>
</div>

<style>
  .http-demo {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }

  .user-filter {
    margin-bottom: 2rem;
    padding: 1rem;
    background: #f5f5f5;
    border-radius: 8px;
  }

  .selected-user {
    margin-top: 0.5rem;
    padding: 0.5rem;
    background: #e3f2fd;
    border-radius: 4px;
  }

  .selected-user button {
    margin-left: 1rem;
    padding: 0.25rem 0.5rem;
    background: #1976d2;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .create-post {
    margin-bottom: 2rem;
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
  }

  .form-group {
    margin-bottom: 1rem;
  }

  .form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: bold;
  }

  .form-group input,
  .form-group textarea,
  .form-group select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
  }

  .form-group button {
    background: #4CAF50;
    color: white;
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1rem;
  }

  .error {
    background: #ffebee;
    color: #c62828;
    padding: 1rem;
    border-radius: 4px;
    margin-bottom: 1rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .error button {
    background: none;
    border: none;
    color: #c62828;
    font-size: 1.5rem;
    cursor: pointer;
  }

  .loading {
    text-align: center;
    padding: 2rem;
  }

  .spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid #4CAF50;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin: 0 auto 1rem;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }

  .posts-list {
    margin-top: 2rem;
  }

  .no-posts {
    text-align: center;
    color: #666;
    padding: 2rem;
  }

  .post {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
    margin-bottom: 1rem;
    background: white;
  }

  .post h4 {
    margin: 0 0 0.5rem 0;
    color: #333;
  }

  .post p {
    margin: 0 0 1rem 0;
    line-height: 1.6;
    color: #666;
  }

  .post-meta {
    display: flex;
    gap: 1rem;
    font-size: 0.875rem;
    color: #888;
    margin-bottom: 1rem;
  }

  .post-actions {
    display: flex;
    gap: 0.5rem;
  }

  .btn-update {
    background: #2196F3;
    color: white;
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .btn-delete {
    background: #f44336;
    color: white;
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .btn-update:hover,
  .btn-delete:hover {
    opacity: 0.8;
  }
</style>

Forms and Validation

<script>
  import { createEventDispatcher } from 'svelte';
  
  const dispatch = createEventDispatcher();
  
  // Form data
  let formData = {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    age: '',
    country: '',
    interests: [],
    newsletter: false,
    terms: false
  };
  
  // Validation errors
  let errors = {};
  let touched = {};
  
  // Available options
  const countries = [
    'United States', 'United Kingdom', 'Canada', 'Australia', 
    'Germany', 'France', 'Japan', 'South Korea'
  ];
  
  const interestOptions = [
    'Technology', 'Sports', 'Music', 'Travel', 
    'Cooking', 'Reading', 'Gaming', 'Art'
  ];
  
  // Validation rules
  function validateField(field, value) {
    switch (field) {
      case 'firstName':
        return value.trim().length < 2 ? 'First name must be at least 2 characters' : '';
      
      case 'lastName':
        return value.trim().length < 2 ? 'Last name must be at least 2 characters' : '';
      
      case 'email':
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return !emailRegex.test(value) ? 'Please enter a valid email address' : '';
      
      case 'password':
        if (value.length < 8) return 'Password must be at least 8 characters';
        if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
          return 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
        }
        return '';
      
      case 'confirmPassword':
        return value !== formData.password ? 'Passwords do not match' : '';
      
      case 'age':
        const ageNum = parseInt(value);
        if (isNaN(ageNum) || ageNum < 13 || ageNum > 120) {
          return 'Age must be between 13 and 120';
        }
        return '';
      
      case 'country':
        return !value ? 'Please select a country' : '';
      
      case 'interests':
        return value.length === 0 ? 'Please select at least one interest' : '';
      
      case 'terms':
        return !value ? 'You must accept the terms and conditions' : '';
      
      default:
        return '';
    }
  }
  
  // Validate all fields
  function validateForm() {
    const newErrors = {};
    
    Object.keys(formData).forEach(field => {
      if (field !== 'newsletter') { // Newsletter is optional
        const error = validateField(field, formData[field]);
        if (error) newErrors[field] = error;
      }
    });
    
    errors = newErrors;
    return Object.keys(newErrors).length === 0;
  }
  
  // Handle field change
  function handleFieldChange(field, value) {
    formData[field] = value;
    touched[field] = true;
    
    // Validate single field
    const error = validateField(field, value);
    if (error) {
      errors[field] = error;
    } else {
      delete errors[field];
      errors = { ...errors }; // Trigger reactivity
    }
    
    // Re-validate confirm password if password changed
    if (field === 'password' && touched.confirmPassword) {
      const confirmError = validateField('confirmPassword', formData.confirmPassword);
      if (confirmError) {
        errors.confirmPassword = confirmError;
      } else {
        delete errors.confirmPassword;
        errors = { ...errors };
      }
    }
  }
  
  // Handle interest toggle
  function toggleInterest(interest) {
    const currentInterests = formData.interests;
    if (currentInterests.includes(interest)) {
      formData.interests = currentInterests.filter(i => i !== interest);
    } else {
      formData.interests = [...currentInterests, interest];
    }
    handleFieldChange('interests', formData.interests);
  }
  
  // Handle form submission
  function handleSubmit() {
    // Mark all fields as touched
    Object.keys(formData).forEach(field => {
      touched[field] = true;
    });
    touched = { ...touched };
    
    if (validateForm()) {
      dispatch('submit', formData);
      console.log('Form submitted:', formData);
      alert('Form submitted successfully!');
    } else {
      console.log('Form has errors:', errors);
    }
  }
  
  // Reset form
  function resetForm() {
    formData = {
      firstName: '',
      lastName: '',
      email: '',
      password: '',
      confirmPassword: '',
      age: '',
      country: '',
      interests: [],
      newsletter: false,
      terms: false
    };
    errors = {};
    touched = {};
  }
  
  // Computed properties
  $: isFormValid = Object.keys(errors).length === 0 && Object.keys(touched).length > 0;
  $: hasErrors = Object.keys(errors).length > 0;
</script>

<div class="form-container">
  <h2>User Registration Form</h2>
  <p>Please fill out all required fields marked with *</p>
  
  <form on:submit|preventDefault={handleSubmit} novalidate>
    <!-- Name fields -->
    <div class="form-row">
      <div class="form-group">
        <label for="firstName">First Name *</label>
        <input
          id="firstName"
          type="text"
          bind:value={formData.firstName}
          on:blur={() => handleFieldChange('firstName', formData.firstName)}
          on:input={(e) => handleFieldChange('firstName', e.target.value)}
          class:error={errors.firstName && touched.firstName}
          placeholder="Enter your first name"
          required
        />
        {#if errors.firstName && touched.firstName}
          <span class="error-message">{errors.firstName}</span>
        {/if}
      </div>
      
      <div class="form-group">
        <label for="lastName">Last Name *</label>
        <input
          id="lastName"
          type="text"
          bind:value={formData.lastName}
          on:blur={() => handleFieldChange('lastName', formData.lastName)}
          on:input={(e) => handleFieldChange('lastName', e.target.value)}
          class:error={errors.lastName && touched.lastName}
          placeholder="Enter your last name"
          required
        />
        {#if errors.lastName && touched.lastName}
          <span class="error-message">{errors.lastName}</span>
        {/if}
      </div>
    </div>
    
    <!-- Email field -->
    <div class="form-group">
      <label for="email">Email Address *</label>
      <input
        id="email"
        type="email"
        bind:value={formData.email}
        on:blur={() => handleFieldChange('email', formData.email)}
        on:input={(e) => handleFieldChange('email', e.target.value)}
        class:error={errors.email && touched.email}
        placeholder="Enter your email address"
        required
      />
      {#if errors.email && touched.email}
        <span class="error-message">{errors.email}</span>
      {/if}
    </div>
    
    <!-- Password fields -->
    <div class="form-row">
      <div class="form-group">
        <label for="password">Password *</label>
        <input
          id="password"
          type="password"
          bind:value={formData.password}
          on:blur={() => handleFieldChange('password', formData.password)}
          on:input={(e) => handleFieldChange('password', e.target.value)}
          class:error={errors.password && touched.password}
          placeholder="Enter your password"
          required
        />
        {#if errors.password && touched.password}
          <span class="error-message">{errors.password}</span>
        {/if}
      </div>
      
      <div class="form-group">
        <label for="confirmPassword">Confirm Password *</label>
        <input
          id="confirmPassword"
          type="password"
          bind:value={formData.confirmPassword}
          on:blur={() => handleFieldChange('confirmPassword', formData.confirmPassword)}
          on:input={(e) => handleFieldChange('confirmPassword', e.target.value)}
          class:error={errors.confirmPassword && touched.confirmPassword}
          placeholder="Confirm your password"
          required
        />
        {#if errors.confirmPassword && touched.confirmPassword}
          <span class="error-message">{errors.confirmPassword}</span>
        {/if}
      </div>
    </div>
    
    <!-- Age and Country -->
    <div class="form-row">
      <div class="form-group">
        <label for="age">Age *</label>
        <input
          id="age"
          type="number"
          bind:value={formData.age}
          on:blur={() => handleFieldChange('age', formData.age)}
          on:input={(e) => handleFieldChange('age', e.target.value)}
          class:error={errors.age && touched.age}
          placeholder="Enter your age"
          min="13"
          max="120"
          required
        />
        {#if errors.age && touched.age}
          <span class="error-message">{errors.age}</span>
        {/if}
      </div>
      
      <div class="form-group">
        <label for="country">Country *</label>
        <select
          id="country"
          bind:value={formData.country}
          on:change={(e) => handleFieldChange('country', e.target.value)}
          class:error={errors.country && touched.country}
          required
        >
          <option value="">Select a country</option>
          {#each countries as country}
            <option value={country}>{country}</option>
          {/each}
        </select>
        {#if errors.country && touched.country}
          <span class="error-message">{errors.country}</span>
        {/if}
      </div>
    </div>
    
    <!-- Interests (checkboxes) -->
    <div class="form-group">
      <label>Interests * (select at least one)</label>
      <div class="checkbox-group" class:error={errors.interests && touched.interests}>
        {#each interestOptions as interest}
          <label class="checkbox-label">
            <input
              type="checkbox"
              checked={formData.interests.includes(interest)}
              on:change={() => toggleInterest(interest)}
            />
            {interest}
          </label>
        {/each}
      </div>
      {#if errors.interests && touched.interests}
        <span class="error-message">{errors.interests}</span>
      {/if}
    </div>
    
    <!-- Newsletter subscription -->
    <div class="form-group">
      <label class="checkbox-label">
        <input
          type="checkbox"
          bind:checked={formData.newsletter}
        />
        Subscribe to our newsletter (optional)
      </label>
    </div>
    
    <!-- Terms and conditions -->
    <div class="form-group">
      <label class="checkbox-label" class:error={errors.terms && touched.terms}>
        <input
          type="checkbox"
          bind:checked={formData.terms}
          on:change={(e) => handleFieldChange('terms', e.target.checked)}
          required
        />
        I accept the <a href="/terms" target="_blank">terms and conditions</a> *
      </label>
      {#if errors.terms && touched.terms}
        <span class="error-message">{errors.terms}</span>
      {/if}
    </div>
    
    <!-- Form actions -->
    <div class="form-actions">
      <button type="button" on:click={resetForm} class="btn-secondary">
        Reset Form
      </button>
      <button 
        type="submit" 
        class="btn-primary"
        disabled={hasErrors || Object.keys(touched).length === 0}
      >
        Create Account
      </button>
    </div>
    
    <!-- Form summary -->
    <div class="form-summary">
      <p>Form Status: {isFormValid ? '✅ Valid' : hasErrors ? '❌ Has Errors' : '⏳ Incomplete'}</p>
      {#if hasErrors}
        <p class="error-count">Errors: {Object.keys(errors).length}</p>
      {/if}
    </div>
  </form>
</div>

<style>
  .form-container {
    max-width: 600px;
    margin: 0 auto;
    padding: 2rem;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  }

  .form-container h2 {
    text-align: center;
    margin-bottom: 0.5rem;
    color: #333;
  }

  .form-container p {
    text-align: center;
    color: #666;
    margin-bottom: 2rem;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }

  .form-group {
    margin-bottom: 1.5rem;
  }

  .form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: 600;
    color: #333;
  }

  .form-group input,
  .form-group select {
    width: 100%;
    padding: 0.75rem;
    border: 2px solid #e1e5e9;
    border-radius: 6px;
    font-size: 1rem;
    transition: border-color 0.2s, box-shadow 0.2s;
  }

  .form-group input:focus,
  .form-group select:focus {
    outline: none;
    border-color: #4CAF50;
    box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
  }

  .form-group input.error,
  .form-group select.error {
    border-color: #f44336;
  }

  .checkbox-group {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 0.5rem;
    padding: 0.75rem;
    border: 2px solid #e1e5e9;
    border-radius: 6px;
    transition: border-color 0.2s;
  }

  .checkbox-group.error {
    border-color: #f44336;
  }

  .checkbox-label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    font-weight: normal !important;
  }

  .checkbox-label.error {
    color: #f44336;
  }

  .checkbox-label input[type="checkbox"] {
    width: auto;
    margin: 0;
  }

  .error-message {
    display: block;
    color: #f44336;
    font-size: 0.875rem;
    margin-top: 0.25rem;
  }

  .form-actions {
    display: flex;
    gap: 1rem;
    justify-content: flex-end;
    margin-top: 2rem;
    padding-top: 2rem;
    border-top: 1px solid #e1e5e9;
  }

  .btn-primary,
  .btn-secondary {
    padding: 0.75rem 2rem;
    border: none;
    border-radius: 6px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s, transform 0.1s;
  }

  .btn-primary {
    background: #4CAF50;
    color: white;
  }

  .btn-primary:hover:not(:disabled) {
    background: #45a049;
    transform: translateY(-1px);
  }

  .btn-primary:disabled {
    background: #cccccc;
    cursor: not-allowed;
    transform: none;
  }

  .btn-secondary {
    background: #f5f5f5;
    color: #333;
    border: 1px solid #ddd;
  }

  .btn-secondary:hover {
    background: #e0e0e0;
    transform: translateY(-1px);
  }

  .form-summary {
    margin-top: 1rem;
    padding: 1rem;
    background: #f9f9f9;
    border-radius: 6px;
    text-align: center;
  }

  .form-summary p {
    margin: 0;
    font-weight: 600;
  }

  .error-count {
    color: #f44336;
  }

  @media (max-width: 768px) {
    .form-row {
      grid-template-columns: 1fr;
    }
    
    .form-actions {
      flex-direction: column;
    }
    
    .checkbox-group {
      grid-template-columns: 1fr;
    }
  }
</style>