Svelte
Web framework with compile-time optimization. Compiles to vanilla JavaScript at build time, achieving high performance without runtime virtual DOM.
GitHub Overview
sveltejs/svelte
web development for the rest of us
Topics
Star History
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
- Svelte Official Website
- Svelte Official Documentation
- Svelte GitHub Repository
- Svelte REPL
- Svelte Society
- Svelte Community
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>