RetroUI Svelte is a comprehensive UI component library designed specifically for Svelte applications, offering a retro aesthetic combined with modern development practices. Built on top of Tailwind CSS and compatible with shadcn-svelte, it provides over 40 meticulously crafted components with 16 beautifully designed themes supporting both light and dark modes. This article focuses on building interactive forms using RetroUI Svelte's form components, including text inputs, buttons, checkboxes, selects, and validation patterns.
This guide walks through creating production-ready forms using RetroUI Svelte with Svelte, covering everything from basic form setup to advanced validation, theming, and user feedback. This is part 24 of a series on using RetroUI Svelte with Svelte.
Prerequisites
Before starting, ensure you have:
- A Svelte project (SvelteKit recommended, but standalone Svelte works too)
- Node.js 18+ and npm/pnpm/yarn installed
- shadcn-svelte installed and configured in your project (RetroUI Svelte requires shadcn-svelte as a base)
- Basic familiarity with Svelte components, reactive statements, and event handling
- Understanding of HTML forms and form validation concepts
- Familiarity with Tailwind CSS (RetroUI Svelte is built on Tailwind)
For this tutorial, we'll work with a SvelteKit project. If you're using standalone Svelte, the examples will work similarly, but you may need to adjust import paths.
Installation
First, ensure you have shadcn-svelte initialized in your project. If you haven't already, run:
npx shadcn-svelte@latest init
This will set up the necessary configuration and dependencies. Once shadcn-svelte is configured, you can add RetroUI Svelte components to your project using the shadcn-svelte CLI with RetroUI's component URLs.
Install RetroUI Svelte components using the following command pattern:
npx shadcn-svelte@latest add https://retroui-svelte.netlify.app/r/button.json
npx shadcn-svelte@latest add https://retroui-svelte.netlify.app/r/input.json
npx shadcn-svelte@latest add https://retroui-svelte.netlify.app/r/checkbox.json
npx shadcn-svelte@latest add https://retroui-svelte.netlify.app/r/select.json
npx shadcn-svelte@latest add https://retroui-svelte.netlify.app/r/label.json
npx shadcn-svelte@latest add https://retroui-svelte.netlify.app/r/card.json
The components will be added to your $lib/components/ui/ directory with RetroUI's retro styling applied. All components come with full TypeScript support.
Project Setup
RetroUI Svelte components work seamlessly with shadcn-svelte's configuration. The components use Tailwind CSS for styling, so ensure your Tailwind configuration is properly set up.
Your components.json should already be configured from the shadcn-svelte init step. RetroUI components will use the same configuration.
To enable RetroUI's themes, you can add theme configuration to your global CSS or layout file. RetroUI supports 16 themes: Green, Orange, Yellow, Teal, Purple, Gold, Coral, Cyan, Blue, Red, Pink, Indigo, Lime, Rose, Sky, and Slate.
Create or update your global styles file:
/* src/app.css or src/app.postcss */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* RetroUI theme variables will be applied automatically */
/* You can customize these if needed */
}
}
First Example: Basic Form
Let's start with a simple contact form using RetroUI Svelte components:
<!-- src/routes/contact.svelte -->
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
let formData = {
name: '',
email: '',
message: ''
};
function handleSubmit() {
console.log('Form submitted:', formData);
// Handle form submission
alert('Form submitted successfully!');
}
</script>
<Card class="w-full max-w-md mx-auto mt-8">
<CardHeader>
<CardTitle>Contact Us</CardTitle>
</CardHeader>
<CardContent>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
<div class="space-y-2">
<Label for="name">Name</Label>
<Input
id="name"
type="text"
placeholder="Enter your name"
bind:value={formData.name}
required
/>
</div>
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="Enter your email"
bind:value={formData.email}
required
/>
</div>
<div class="space-y-2">
<Label for="message">Message</Label>
<textarea
id="message"
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Enter your message"
bind:value={formData.message}
required
/>
</div>
<Button type="submit" class="w-full">
Submit
</Button>
</form>
</CardContent>
</Card>
This example demonstrates:
- Input: RetroUI's styled text input component with retro aesthetic
- Button: RetroUI's button component with retro styling
- Label: Properly associated labels for accessibility
- Card: Container component for form layout
-
Two-way binding: Using
bind:valueto sync form state - Form handling: Standard Svelte form submission pattern
The components automatically include RetroUI's retro styling, proper accessibility attributes, and focus management.
Understanding Form Components
RetroUI Svelte provides several form-related components that work together:
Input Component
The Input component is the primary text input component with RetroUI styling:
<script lang="ts">
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
let value = $state('');
let email = $state('');
</script>
<!-- Basic input -->
<Input placeholder="Enter text" bind:value={value} />
<!-- With label -->
<div class="space-y-2">
<Label for="email">Email</Label>
<Input id="email" type="email" bind:value={email} />
</div>
<!-- Disabled state -->
<Input disabled placeholder="Disabled input" />
<!-- Different input types -->
<Input type="password" placeholder="Password" />
<Input type="number" placeholder="Enter number" />
<Input type="date" />
Checkbox Component
For boolean choices:
<script lang="ts">
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Label } from '$lib/components/ui/label/index.js';
let termsAccepted = $state(false);
</script>
<div class="flex items-center space-x-2">
<Checkbox id="terms" bind:checked={termsAccepted} />
<Label for="terms" class="cursor-pointer">
I agree to the terms and conditions
</Label>
</div>
Select Component
For dropdown selections:
<script lang="ts">
import * as Select from '$lib/components/ui/select/index.js';
let selectedValue = $state<string | undefined>();
</script>
<Select.Root bind:selected={selectedValue}>
<Select.Trigger class="w-[180px]">
<Select.Value placeholder="Select an option" />
</Select.Trigger>
<Select.Content>
<Select.Item value="option1">Option 1</Select.Item>
<Select.Item value="option2">Option 2</Select.Item>
<Select.Item value="option3">Option 3</Select.Item>
</Select.Content>
</Select.Root>
Practical Example: Registration Form
Let's build a complete registration form with validation and error handling:
<!-- src/routes/register.svelte -->
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
let formData = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: false
};
let errors = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: ''
};
function validateEmail(email: string): boolean {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function validateForm(): boolean {
let isValid = true;
errors = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: ''
};
if (!formData.firstName.trim()) {
errors.firstName = 'First name is required';
isValid = false;
}
if (!formData.lastName.trim()) {
errors.lastName = 'Last name is required';
isValid = false;
}
if (!formData.email.trim()) {
errors.email = 'Email is required';
isValid = false;
} else if (!validateEmail(formData.email)) {
errors.email = 'Please enter a valid email address';
isValid = false;
}
if (!formData.password) {
errors.password = 'Password is required';
isValid = false;
} else if (formData.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
isValid = false;
}
if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
isValid = false;
}
if (!formData.acceptTerms) {
errors.acceptTerms = 'You must accept the terms and conditions';
isValid = false;
}
return isValid;
}
function handleSubmit() {
if (validateForm()) {
console.log('Registration data:', formData);
// Submit to your API
alert('Registration successful!');
}
}
function handleReset() {
formData = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: false
};
errors = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
acceptTerms: ''
};
}
</script>
<div class="container mx-auto py-8 px-4">
<Card class="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Create Account</CardTitle>
<CardDescription>Fill in your information to get started</CardDescription>
</CardHeader>
<CardContent>
<form on:submit|preventDefault={handleSubmit} class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="firstName">First Name</Label>
<Input
id="firstName"
type="text"
placeholder="John"
bind:value={formData.firstName}
class={errors.firstName ? 'border-red-500' : ''}
/>
{#if errors.firstName}
<p class="text-sm text-red-500">{errors.firstName}</p>
{/if}
</div>
<div class="space-y-2">
<Label for="lastName">Last Name</Label>
<Input
id="lastName"
type="text"
placeholder="Doe"
bind:value={formData.lastName}
class={errors.lastName ? 'border-red-500' : ''}
/>
{#if errors.lastName}
<p class="text-sm text-red-500">{errors.lastName}</p>
{/if}
</div>
</div>
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="john.doe@example.com"
bind:value={formData.email}
class={errors.email ? 'border-red-500' : ''}
/>
{#if errors.email}
<p class="text-sm text-red-500">{errors.email}</p>
{/if}
</div>
<div class="space-y-2">
<Label for="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Enter password"
bind:value={formData.password}
class={errors.password ? 'border-red-500' : ''}
/>
{#if errors.password}
<p class="text-sm text-red-500">{errors.password}</p>
{/if}
<p class="text-sm text-muted-foreground">Must be at least 8 characters</p>
</div>
<div class="space-y-2">
<Label for="confirmPassword">Confirm Password</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Confirm password"
bind:value={formData.confirmPassword}
class={errors.confirmPassword ? 'border-red-500' : ''}
/>
{#if errors.confirmPassword}
<p class="text-sm text-red-500">{errors.confirmPassword}</p>
{/if}
</div>
<div class="space-y-2">
<div class="flex items-center space-x-2">
<Checkbox id="terms" bind:checked={formData.acceptTerms} />
<Label for="terms" class="cursor-pointer">
I accept the terms and conditions
</Label>
</div>
{#if errors.acceptTerms}
<p class="text-sm text-red-500">{errors.acceptTerms}</p>
{/if}
</div>
<div class="flex gap-4 pt-4">
<Button type="submit" class="flex-1">
Register
</Button>
<Button type="button" variant="outline" on:click={handleReset}>
Reset
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
This example demonstrates:
- Complete form validation: Client-side validation with error messages
- Error state handling: Visual feedback for validation errors
- Form reset: Clearing form data and errors
- Responsive layout: Grid layout that adapts to screen size
- RetroUI styling: Automatic retro aesthetic from RetroUI components
- Accessibility: Proper labels and form structure
Advanced: Form with Select and Theming
For more complex forms, RetroUI Svelte provides additional components and theme support:
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
let formData = {
name: '',
country: '',
theme: 'blue'
};
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'ca', label: 'Canada' },
{ value: 'au', label: 'Australia' }
];
const themes = [
{ value: 'green', label: 'Green' },
{ value: 'orange', label: 'Orange' },
{ value: 'blue', label: 'Blue' },
{ value: 'purple', label: 'Purple' },
{ value: 'red', label: 'Red' },
{ value: 'pink', label: 'Pink' }
];
function handleSubmit() {
console.log('Form data:', formData);
}
</script>
<Card class="w-full max-w-md mx-auto mt-8">
<CardHeader>
<CardTitle>Preferences</CardTitle>
</CardHeader>
<CardContent>
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
<div class="space-y-2">
<Label for="name">Full Name</Label>
<Input
id="name"
type="text"
placeholder="Enter your name"
bind:value={formData.name}
required
/>
</div>
<div class="space-y-2">
<Label for="country">Country</Label>
<Select.Root bind:selected={formData.country}>
<Select.Trigger class="w-full">
<Select.Value placeholder="Select a country" />
</Select.Trigger>
<Select.Content>
{#each countries as country}
<Select.Item value={country.value}>
{country.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label for="theme">Preferred Theme</Label>
<Select.Root bind:selected={formData.theme}>
<Select.Trigger class="w-full">
<Select.Value placeholder="Select a theme" />
</Select.Trigger>
<Select.Content>
{#each themes as theme}
<Select.Item value={theme.value}>
{theme.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Button type="submit" class="w-full">
Save Preferences
</Button>
</form>
</CardContent>
</Card>
Common Issues / Troubleshooting
Components not found or import errors
- Ensure shadcn-svelte is properly initialized:
npx shadcn-svelte@latest init - Verify the component was added successfully: check
$lib/components/ui/directory - Make sure you're using the correct import path:
$lib/components/ui/[component]/index.js - For SvelteKit, ensure your
src/libalias is configured invite.config.js
Form values not updating
- Ensure you're using
bind:value(not justvalue) for two-way binding - Check that the variable is declared with
letor$state()for Svelte 5 - Verify the component name matches RetroUI's API (case-sensitive)
- For Svelte 5, use
$state()instead ofletfor reactive state
Styling conflicts or theme not applying
- RetroUI uses Tailwind CSS - ensure Tailwind is properly configured
- Check that Tailwind directives are included in your CSS:
@tailwind base; @tailwind components; @tailwind utilities; - Verify
components.jsonconfiguration matches your project structure - Clear build cache and restart dev server if styles aren't updating
Select component not working
- Ensure you're using the full Select API:
Select.Root,Select.Trigger,Select.Content,Select.Item - Check that
bind:selectedis used (notbind:value) - Verify the value type matches between
selectedandSelect.Itemvalues
Next Steps
Now that you understand how to build forms with RetroUI Svelte, consider exploring:
- Form state management: Integrating with stores or form libraries like Felte or Svelte Forms Lib
- Server-side validation: Connecting client forms to API endpoints with validation using SvelteKit form actions
- Advanced components: Explore RetroUI's other components like Data Tables, Navigation Drawers, and Dialogs
- Theming: Customizing RetroUI's 16 themes and implementing theme switching
- Accessibility: Testing forms with screen readers and keyboard navigation
- Form libraries integration: Using RetroUI components with form validation libraries like sveltekit-superforms
- Dark mode: Implementing dark mode support with RetroUI's theme system
For more information, visit the RetroUI Svelte documentation and explore the shadcn-svelte documentation for underlying component APIs.
Summary
RetroUI Svelte provides a powerful set of retro-styled components that make building forms in Svelte straightforward and visually appealing. You should now be able to create forms with text inputs, validation, error handling, and various input types. The components handle styling, accessibility, and user interaction patterns automatically, allowing you to focus on your application logic while maintaining a unique retro aesthetic.
Top comments (0)