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:
< 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).
Now let’s build a more realistic example: a contact form with validation and multiple components.
Import the components
First, import all the components you’ll need: < script lang = "ts" >
import {
InputText ,
InputTextarea ,
BaseButton ,
BaseCard ,
Notification
} from '@invopop/popui'
</ script >
Set up reactive state
Create reactive variables for form data and validation using Svelte 5’s runes: < 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 >
Add validation logic
Implement form validation: < 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 >
Create the form UI
Build the form using Popui components: < 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:
< 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:
Primary
Outline
Danger
Ghost
< 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
Use TypeScript - Popui is fully typed, making development easier with autocompletion and type checking
Check Storybook - When in doubt about component usage, the Storybook has working examples
Customize the theme - Import and modify the Tailwind theme to match your brand
Explore component props - Most components accept additional HTML attributes via ...rest
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 }
< 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!