Skeleton is a powerful UI toolkit built with Svelte and Tailwind CSS that enables developers to create adaptive, accessible design systems for web applications. This guide walks through building accessible and styled forms using Skeleton with Svelte, covering everything from basic input elements to form validation and input groups. This is part 5 of a series on using Skeleton with Svelte.
Prerequisites
Before starting, ensure you have:
- Node.js version 18.x or higher installed
- SvelteKit project set up (or basic Svelte project)
- Basic understanding of Svelte components and reactivity
- Familiarity with Tailwind CSS utility classes
Key Concepts to Understand:
-
Input elements: Native HTML form inputs (
<input>,<textarea>,<select>) that Skeleton styles automatically - Input groups: Combined input elements with addons, buttons, or dividers for enhanced UX
- Form validation: Client-side validation using Svelte's reactive statements and bindings
Installation
If you're starting a new project, the easiest way is to use the Skeleton CLI:
npm create skeleton-app@latest my-skeleton-app
cd my-skeleton-app
For an existing SvelteKit project, install Skeleton packages manually:
npm install @skeletonlabs/skeleton @skeletonlabs/tw-plugin
npm install -D tailwindcss postcss autoprefixer @tailwindcss/forms
The CLI automatically handles:
- Installation of Skeleton packages
- Tailwind CSS v3 setup
- Basic Skeleton and Tailwind configuration
- Theme customization options
Project Setup
After installation, configure Tailwind to use the Skeleton plugin. Update your tailwind.config.js:
// tailwind.config.js
import { skeleton } from '@skeletonlabs/tw-plugin';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
'./node_modules/@skeletonlabs/skeleton/**/*.{html,js,svelte,ts}'
],
theme: {
extend: {},
},
plugins: [
skeleton({
themes: {
preset: [
{
name: 'skeleton',
enhancements: true,
},
],
},
}),
require('@tailwindcss/forms'),
],
};
Ensure your main CSS file imports Skeleton's base styles:
/* src/app.css or src/app.postcss */
@import '@skeletonlabs/skeleton/themes/theme-skeleton.css';
@import '@skeletonlabs/skeleton/styles/skeleton.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
First Example / Basic Usage
Let's start with a simple contact form to see Skeleton's form styling in action:
<!-- src/routes/ContactForm.svelte -->
<script>
let name = '';
let email = '';
let message = '';
function handleSubmit() {
console.log({ name, email, message });
// Handle form submission
}
</script>
<form on:submit|preventDefault={handleSubmit} class="card p-6 max-w-md mx-auto">
<h2 class="text-2xl font-bold mb-4">Contact Us</h2>
<div class="mb-4">
<label for="name" class="label">Name</label>
<input
type="text"
id="name"
bind:value={name}
placeholder="Your name"
class="input"
required
/>
</div>
<div class="mb-4">
<label for="email" class="label">Email</label>
<input
type="email"
id="email"
bind:value={email}
placeholder="your.email@example.com"
class="input"
required
/>
</div>
<div class="mb-4">
<label for="message" class="label">Message</label>
<textarea
id="message"
bind:value={message}
placeholder="Your message"
class="input"
rows="5"
required
></textarea>
</div>
<button type="submit" class="btn variant-filled-primary">
Send Message
</button>
</form>
What's happening here:
-
class="input"applies Skeleton's default input styling -
class="label"styles the form labels consistently -
class="btn variant-filled-primary"creates a primary button with Skeleton's button variants - Svelte's
bind:valuecreates two-way data binding for form inputs - The form automatically gets Skeleton's theme colors and spacing
Understanding the Basics
Skeleton provides styling for native HTML form elements through utility classes. The key classes are:
-
.input: Styles text inputs, textareas, and select elements -
.label: Consistent label styling -
.btn: Button base class with variant modifiers -
.input-group: Container for grouped inputs with addons
Here's an example showing different input types:
<!-- src/routes/InputTypes.svelte -->
<script>
let textValue = '';
let emailValue = '';
let passwordValue = '';
let dateValue = '';
let numberValue = 0;
</script>
<div class="card p-6 max-w-md mx-auto space-y-4">
<h2 class="text-2xl font-bold mb-4">Input Types</h2>
<!-- Text input -->
<div>
<label for="text" class="label">Text</label>
<input type="text" id="text" bind:value={textValue} placeholder="Enter text" class="input" />
</div>
<!-- Email input -->
<div>
<label for="email" class="label">Email</label>
<input type="email" id="email" bind:value={emailValue} placeholder="email@example.com" class="input" />
</div>
<!-- Password input -->
<div>
<label for="password" class="label">Password</label>
<input type="password" id="password" bind:value={passwordValue} placeholder="Password" class="input" />
</div>
<!-- Date input -->
<div>
<label for="date" class="label">Date</label>
<input type="date" id="date" bind:value={dateValue} class="input" />
</div>
<!-- Number input -->
<div>
<label for="number" class="label">Number</label>
<input type="number" id="number" bind:value={numberValue} placeholder="0" class="input" />
</div>
<!-- Readonly and disabled states -->
<div>
<label for="readonly" class="label">Readonly</label>
<input type="text" id="readonly" value="Cannot edit" readonly class="input" />
</div>
<div>
<label for="disabled" class="label">Disabled</label>
<input type="text" id="disabled" value="Disabled" disabled class="input" />
</div>
</div>
Skeleton automatically styles all these input types consistently, and the .input class handles focus states, borders, and theme colors.
Practical Example / Building Something Real
Let's build a complete user registration form with validation, input groups, and multiple field types:
<!-- src/routes/RegistrationForm.svelte -->
<script>
let formData = {
username: '',
email: '',
password: '',
confirmPassword: '',
website: '',
age: '',
bio: ''
};
let errors = {};
let isSubmitting = false;
function validateForm() {
errors = {};
if (!formData.username || formData.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
}
if (!formData.email || !formData.email.includes('@')) {
errors.email = 'Please enter a valid email address';
}
if (!formData.password || formData.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
}
if (!formData.age || parseInt(formData.age) < 18) {
errors.age = 'You must be at least 18 years old';
}
return Object.keys(errors).length === 0;
}
async function handleSubmit() {
if (!validateForm()) {
return;
}
isSubmitting = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', formData);
isSubmitting = false;
alert('Registration successful!');
}
</script>
<form on:submit|preventDefault={handleSubmit} class="card p-8 max-w-2xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Create Account</h1>
<!-- Username with input group -->
<div class="mb-4">
<label for="username" class="label">Username</label>
<div class="input-group">
<span class="input-group-addon">@</span>
<input
type="text"
id="username"
bind:value={formData.username}
placeholder="username"
class="input"
class:input-error={errors.username}
/>
</div>
{#if errors.username}
<p class="text-sm text-error mt-1">{errors.username}</p>
{/if}
</div>
<!-- Email -->
<div class="mb-4">
<label for="email" class="label">Email</label>
<input
type="email"
id="email"
bind:value={formData.email}
placeholder="your.email@example.com"
class="input"
class:input-error={errors.email}
/>
{#if errors.email}
<p class="text-sm text-error mt-1">{errors.email}</p>
{/if}
</div>
<!-- Password -->
<div class="mb-4">
<label for="password" class="label">Password</label>
<input
type="password"
id="password"
bind:value={formData.password}
placeholder="Minimum 8 characters"
class="input"
class:input-error={errors.password}
/>
{#if errors.password}
<p class="text-sm text-error mt-1">{errors.password}</p>
{/if}
</div>
<!-- Confirm Password -->
<div class="mb-4">
<label for="confirmPassword" class="label">Confirm Password</label>
<input
type="password"
id="confirmPassword"
bind:value={formData.confirmPassword}
placeholder="Re-enter password"
class="input"
class:input-error={errors.confirmPassword}
/>
{#if errors.confirmPassword}
<p class="text-sm text-error mt-1">{errors.confirmPassword}</p>
{/if}
</div>
<!-- Website with input group -->
<div class="mb-4">
<label for="website" class="label">Website (Optional)</label>
<div class="input-group">
<span class="input-group-addon">https://</span>
<input
type="url"
id="website"
bind:value={formData.website}
placeholder="yourwebsite.com"
class="input"
/>
</div>
</div>
<!-- Age -->
<div class="mb-4">
<label for="age" class="label">Age</label>
<input
type="number"
id="age"
bind:value={formData.age}
placeholder="18"
min="18"
class="input"
class:input-error={errors.age}
/>
{#if errors.age}
<p class="text-sm text-error mt-1">{errors.age}</p>
{/if}
</div>
<!-- Bio textarea -->
<div class="mb-6">
<label for="bio" class="label">Bio (Optional)</label>
<textarea
id="bio"
bind:value={formData.bio}
placeholder="Tell us about yourself"
class="input"
rows="4"
></textarea>
</div>
<!-- Submit button -->
<button
type="submit"
class="btn variant-filled-primary w-full"
disabled={isSubmitting}
>
{isSubmitting ? 'Creating Account...' : 'Create Account'}
</button>
</form>
<style>
:global(.input-error) {
@apply border-error;
}
</style>
Key features of this form:
-
Input groups: Username and website fields use
.input-groupwith addons - Validation: Client-side validation with error messages
-
Error states: Conditional styling with
class:input-errorbinding - Multiple input types: Text, email, password, URL, number, and textarea
- Loading state: Disabled button during submission
- Accessibility: Proper labels and form structure
Common Issues / Troubleshooting
1. Inputs not styling correctly
Problem: Inputs don't have Skeleton styling applied.
Solution: Ensure you've:
- Imported Skeleton CSS in your main stylesheet
- Added
@tailwindcss/formsplugin totailwind.config.js - Used the
.inputclass on your input elements
2. Input groups not aligning properly
Problem: Input group addons appear misaligned.
Solution: Make sure all elements (addon, input, button) are direct children of .input-group:
<div class="input-group">
<span class="input-group-addon">@</span>
<input type="text" class="input" />
</div>
3. Form validation not working
Problem: Validation errors don't appear or update.
Solution: Use Svelte's reactive statements ($:) to trigger validation on value changes:
$: if (formData.email) {
validateEmail();
}
4. Theme colors not applying
Problem: Form elements use default colors instead of theme.
Solution: Verify the Skeleton plugin is configured in tailwind.config.js and the theme CSS is imported before Tailwind directives.
Next Steps
Now that you understand form basics with Skeleton, consider exploring:
- Advanced form components: Checkboxes, radio buttons, and select dropdowns
- Form libraries integration: Combining Skeleton with form libraries like Felte or Svelte-forms-lib
- Server-side validation: Integrating with SvelteKit form actions
- Accessibility enhancements: ARIA attributes and keyboard navigation
- Other articles in this series: Building navigation, creating data tables, or setting up themes
Resources:
Summary
You've learned how to build accessible, styled forms with Skeleton in Svelte. You can now create forms with various input types, implement validation, use input groups for enhanced UX, and style everything consistently with Skeleton's design system. The combination of Svelte's reactivity and Skeleton's utility classes makes form development straightforward and maintainable.
Top comments (0)