Skip to main content

Quick Start

This guide will walk you through creating your first UI with Popui components. We’ll build a simple form with validation to demonstrate the key concepts.

Your First Component

Let’s start with a simple button to verify everything works:
+page.svelte
<script>
  import { BaseButton } from '@invopop/popui'

  function handleClick() {
    console.log('Button clicked!')
  }
</script>

<BaseButton variant="primary" onclick={handleClick}>
  Click Me
</BaseButton>
Popui uses Svelte 5’s event handlers with lowercase syntax (e.g., onclick instead of on:click).

Building a Complete Form

Now let’s build a more realistic example: a contact form with validation and multiple components.
1

Import the components

First, import all the components you’ll need:
ContactForm.svelte
<script lang="ts">
  import { 
    InputText, 
    InputTextarea, 
    BaseButton,
    BaseCard,
    Notification
  } from '@invopop/popui'
</script>
2

Set up reactive state

Create reactive variables for form data and validation using Svelte 5’s runes:
ContactForm.svelte
<script lang="ts">
  import { 
    InputText, 
    InputTextarea, 
    BaseButton,
    BaseCard,
    Toaster,
    toast
  } from '@invopop/popui'

  // Form state using $state rune
  let name = $state('')
  let email = $state('')
  let message = $state('')

  // Error states
  let nameError = $state('')
  let emailError = $state('')
  let messageError = $state('')

  // Loading state
  let isSubmitting = $state(false)
</script>
3

Add validation logic

Implement form validation:
ContactForm.svelte
<script lang="ts">
  // ... previous imports and state

  function validateForm(): boolean {
    let isValid = true

    // Reset errors
    nameError = ''
    emailError = ''
    messageError = ''

    // Name validation
    if (!name.trim()) {
      nameError = 'Name is required'
      isValid = false
    }

    // Email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!email.trim()) {
      emailError = 'Email is required'
      isValid = false
    } else if (!emailRegex.test(email)) {
      emailError = 'Please enter a valid email'
      isValid = false
    }

    // Message validation
    if (!message.trim()) {
      messageError = 'Message is required'
      isValid = false
    } else if (message.length < 10) {
      messageError = 'Message must be at least 10 characters'
      isValid = false
    }

    return isValid
  }

  async function handleSubmit() {
    if (!validateForm()) return

    isSubmitting = true

    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1500))

      // Success! Reset form
      name = ''
      email = ''
      message = ''
      
      alert('Form submitted successfully!')
    } catch (error) {
      console.error('Submission error:', error)
    } finally {
      isSubmitting = false
    }
  }
</script>
4

Create the form UI

Build the form using Popui components:
ContactForm.svelte
<div class="max-w-2xl mx-auto p-6">
  <BaseCard
    title="Contact Us"
    description="Send us a message and we'll get back to you soon."
  >
    <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6">
      <InputText
        label="Name"
        bind:value={name}
        placeholder="John Doe"
        errorText={nameError}
        disabled={isSubmitting}
      />

      <InputText
        label="Email"
        bind:value={email}
        placeholder="[email protected]"
        errorText={emailError}
        disabled={isSubmitting}
      />

      <InputTextarea
        label="Message"
        bind:value={message}
        placeholder="Tell us what you're thinking..."
        errorText={messageError}
        disabled={isSubmitting}
        rows={5}
      />

      <div class="flex gap-3 justify-end">
        <BaseButton
          variant="outline"
          onclick={() => {
            name = ''
            email = ''
            message = ''
            nameError = ''
            emailError = ''
            messageError = ''
          }}
          disabled={isSubmitting}
        >
          Reset
        </BaseButton>

        <BaseButton
          type="submit"
          variant="primary"
          disabled={isSubmitting}
        >
          {isSubmitting ? 'Sending...' : 'Send Message'}
        </BaseButton>
      </div>
    </form>
  </BaseCard>
</div>

Complete Example

Here’s the full working component:
ContactForm.svelte
<script lang="ts">
  import { 
    InputText, 
    InputTextarea, 
    BaseButton,
    BaseCard
  } from '@invopop/popui'

  let name = $state('')
  let email = $state('')
  let message = $state('')
  let nameError = $state('')
  let emailError = $state('')
  let messageError = $state('')
  let isSubmitting = $state(false)

  function validateForm(): boolean {
    let isValid = true
    nameError = ''
    emailError = ''
    messageError = ''

    if (!name.trim()) {
      nameError = 'Name is required'
      isValid = false
    }

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!email.trim()) {
      emailError = 'Email is required'
      isValid = false
    } else if (!emailRegex.test(email)) {
      emailError = 'Please enter a valid email'
      isValid = false
    }

    if (!message.trim()) {
      messageError = 'Message is required'
      isValid = false
    } else if (message.length < 10) {
      messageError = 'Message must be at least 10 characters'
      isValid = false
    }

    return isValid
  }

  async function handleSubmit() {
    if (!validateForm()) return

    isSubmitting = true
    try {
      await new Promise(resolve => setTimeout(resolve, 1500))
      name = ''
      email = ''
      message = ''
      alert('Form submitted successfully!')
    } catch (error) {
      console.error('Submission error:', error)
    } finally {
      isSubmitting = false
    }
  }

  function resetForm() {
    name = ''
    email = ''
    message = ''
    nameError = ''
    emailError = ''
    messageError = ''
  }
</script>

<div class="max-w-2xl mx-auto p-6">
  <BaseCard
    title="Contact Us"
    description="Send us a message and we'll get back to you soon."
  >
    <form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6">
      <InputText
        label="Name"
        bind:value={name}
        placeholder="John Doe"
        errorText={nameError}
        disabled={isSubmitting}
      />

      <InputText
        label="Email"
        bind:value={email}
        placeholder="[email protected]"
        errorText={emailError}
        disabled={isSubmitting}
      />

      <InputTextarea
        label="Message"
        bind:value={message}
        placeholder="Tell us what you're thinking..."
        errorText={messageError}
        disabled={isSubmitting}
        rows={5}
      />

      <div class="flex gap-3 justify-end">
        <BaseButton
          variant="outline"
          onclick={resetForm}
          disabled={isSubmitting}
        >
          Reset
        </BaseButton>

        <BaseButton
          type="submit"
          variant="primary"
          disabled={isSubmitting}
        >
          {isSubmitting ? 'Sending...' : 'Send Message'}
        </BaseButton>
      </div>
    </form>
  </BaseCard>
</div>

Key Concepts

Svelte 5 Runes

Popui uses Svelte 5’s $state, $derived, and $props runes for reactivity. Learn more in the Svelte 5 docs.

Event Handlers

Use lowercase event handlers like onclick, oninput, onblur instead of the old on: directive syntax.

Two-way Binding

Use bind:value for two-way data binding with form components.

Error States

Pass error messages via the errorText prop to display validation errors.

Component Variants

Most Popui components support multiple variants. Here are examples with BaseButton:
<BaseButton variant="primary">
  Primary Action
</BaseButton>

Adding Icons

Popui components support icons from the @invopop/ui-icons and @steeze-ui/heroicons packages:
<script>
  import { BaseButton } from '@invopop/popui'
  import { Add, Close } from '@invopop/ui-icons'
  import { Cog6Tooth } from '@steeze-ui/heroicons'
</script>

<BaseButton variant="primary" icon={Add}>
  Add Item
</BaseButton>

<BaseButton variant="outline" icon={Cog6Tooth}>
  Settings
</BaseButton>

<BaseButton variant="danger" icon={Close}>
  Delete
</BaseButton>

Working with Data Tables

For complex data display, use the DataTable component:
<script lang="ts">
  import { DataTable, createSvelteTable } from '@invopop/popui'
  import type { ColumnDef } from '@tanstack/table-core'

  type User = {
    id: string
    name: string
    email: string
    role: string
  }

  const data: User[] = [
    { id: '1', name: 'John Doe', email: '[email protected]', role: 'Admin' },
    { id: '2', name: 'Jane Smith', email: '[email protected]', role: 'User' },
    { id: '3', name: 'Bob Johnson', email: '[email protected]', role: 'User' }
  ]

  const columns: ColumnDef<User>[] = [
    {
      accessorKey: 'name',
      header: 'Name'
    },
    {
      accessorKey: 'email',
      header: 'Email'
    },
    {
      accessorKey: 'role',
      header: 'Role'
    }
  ]

  const table = createSvelteTable({
    data,
    columns
  })
</script>

<DataTable {table} />

Next Steps

View All Components

Explore the complete component library in Storybook

GitHub Repository

View source code and contribute
For detailed API documentation of each component, visit the Storybook where you can interact with live examples and see all available props.

Tips for Success

  1. Use TypeScript - Popui is fully typed, making development easier with autocompletion and type checking
  2. Check Storybook - When in doubt about component usage, the Storybook has working examples
  3. Customize the theme - Import and modify the Tailwind theme to match your brand
  4. Explore component props - Most components accept additional HTML attributes via ...rest
  5. Handle events properly - Remember to use lowercase event handlers (onclick, not on:click)

Common Patterns

Conditional Rendering

{#if isLoading}
  <Skeleton />
{:else}
  <DataTable {table} />
{/if}

Lists and Iteration

{#each items as item}
  <DataListItem
    title={item.title}
    description={item.description}
    onclick={() => handleItemClick(item)}
  />
{/each}

Tooltips

<Tooltip>
  <TooltipTrigger>
    <BaseButton variant="ghost" icon={InfoIcon} />
  </TooltipTrigger>
  <TooltipContent>
    <p>Additional information about this feature</p>
  </TooltipContent>
</Tooltip>
You’re now ready to build beautiful UIs with Popui!

Build docs developers (and LLMs) love