DEV Community

Lucas Bennett
Lucas Bennett

Posted on

Building Forms with RetroUI Svelte

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 */
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:value to 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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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/lib alias is configured in vite.config.js

Form values not updating

  • Ensure you're using bind:value (not just value) for two-way binding
  • Check that the variable is declared with let or $state() for Svelte 5
  • Verify the component name matches RetroUI's API (case-sensitive)
  • For Svelte 5, use $state() instead of let for 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.json configuration 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:selected is used (not bind:value)
  • Verify the value type matches between selected and Select.Item values

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)