STDF is a mobile web component library built with Svelte v5, Tailwind CSS v4, and TypeScript, offering simple, tiny, well-designed, and fast-performing components with no virtual DOM. This guide walks through building custom composite components by combining multiple STDF components to create reusable, production-ready UI patterns. This is part 14 of a series on using STDF with Svelte.
Prerequisites
Before starting, ensure you have:
- Node.js version 18.x or higher installed
- SvelteKit project set up (or Svelte v5 project with Vite)
- Svelte v5 (STDF requires Svelte v5)
- Tailwind CSS v4 installed and configured
- Solid understanding of Svelte component composition, reactivity, and props
- Familiarity with TypeScript (recommended but not required)
Key Concepts to Understand:
- Component composition: Combining multiple STDF components (like Dialog, Input, Button) to create more complex, reusable components
- State management: Using Svelte's reactive statements and stores to manage component state across composed components
- Event handling: Passing events between parent and child components, and handling user interactions in composite components
-
Props forwarding: Using Svelte's
$$propsand$$restPropsto create flexible component APIs that can pass props to underlying STDF components
Installation
Install STDF and its peer dependencies using your preferred package manager:
pnpm add stdf
pnpm add -D svelte@^5.0.0 tailwindcss@^4.0.0
Or with npm:
npm install stdf
npm install -D svelte@^5.0.0 tailwindcss@^4.0.0
Or with yarn:
yarn add stdf
yarn add -D svelte@^5.0.0 tailwindcss@^4.0.0
This will add STDF to your package.json dependencies along with the required peer dependencies.
Project Setup
1. Configure Tailwind CSS
Create or update your main CSS file (typically src/app.css or src/app.postcss) to include Tailwind CSS configuration for STDF:
/* src/app.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Theme Colors */
--color-primary-50: oklch(0.979 0.01 267.36);
--color-primary-100: oklch(0.95 0.024 270.343);
--color-primary-200: oklch(0.847 0.074 271.188);
--color-primary-300: oklch(0.741 0.13 272.232);
--color-primary-400: oklch(0.634 0.193 271.595);
--color-primary-500: oklch(0.536 0.252 268.66);
--color-primary: oklch(0.467 0.296 264.886);
--color-primary-700: oklch(0.397 0.26 264.877);
--color-primary-800: oklch(0.331 0.221 264.833);
--color-primary-900: oklch(0.26 0.178 264.428);
--color-primary-950: oklch(0.192 0.13 266.64);
/* Functional Colors */
--color-success: oklch(0.704 0.142 167.084);
--color-warning: oklch(0.558 0.154 47.186);
--color-error: oklch(0.564 0.223 28.46);
--color-info: oklch(0.482 0.14 261.518);
/* Neutral Colors */
--color-black: oklch(0 0 0);
--color-white: oklch(1 0 0);
--color-gray-50: oklch(0.961 0 0);
--color-gray-100: oklch(0.925 0 0);
--color-gray-200: oklch(0.845 0 0);
--color-gray-300: oklch(0.767 0 0);
--color-gray-400: oklch(0.683 0 0);
--color-gray-500: oklch(0.6 0 0);
--color-gray-600: oklch(0.51 0 0);
--color-gray-700: oklch(0.42 0 0);
--color-gray-800: oklch(0.321 0 0);
--color-gray-900: oklch(0.218 0 0);
--color-gray-950: oklch(0.159 0 0);
--color-transparent: transparent;
}
@source "../node_modules/stdf/**/*.svelte";
2. Import CSS in Your App
Make sure to import the CSS file in your main application entry point:
<!-- src/app.html or src/routes/+layout.svelte -->
<script>
import '../app.css';
</script>
First Example / Basic Usage
Let's start with a simple composite component that combines STDF's Dialog, Input, and Button components to create a reusable confirmation dialog with input:
<!-- src/lib/components/ConfirmInputDialog.svelte -->
<script>
import { Dialog, Input, Button } from 'stdf';
export let visible = $bindable(false);
export let title = 'Confirm Action';
export let message = 'Please enter your confirmation text:';
export let confirmText = 'Confirm';
export let cancelText = 'Cancel';
export let inputPlaceholder = 'Type to confirm';
export let requiredText = '';
let inputValue = '';
let isValid = false;
$: isValid = inputValue === requiredText;
function handleConfirm() {
if (isValid) {
// Dispatch custom event with input value
// Parent component can listen to this
visible = false;
inputValue = '';
}
}
function handleCancel() {
visible = false;
inputValue = '';
}
function handleClose() {
visible = false;
inputValue = '';
}
</script>
<Dialog
bind:visible
{title}
content={message}
primaryText={confirmText}
secondaryText={cancelText}
onprimary={handleConfirm}
onsecondary={handleCancel}
onclose={handleClose}
>
<div class="mt-4">
<Input
bind:value={inputValue}
placeholder={inputPlaceholder}
onchange={() => {}}
/>
{#if inputValue && !isValid}
<p class="text-error text-sm mt-2">
Input does not match required text
</p>
{/if}
</div>
</Dialog>
Usage in a parent component:
<!-- src/routes/example/+page.svelte -->
<script>
import ConfirmInputDialog from '$lib/components/ConfirmInputDialog.svelte';
import { Button } from 'stdf';
let showDialog = false;
function openDialog() {
showDialog = true;
}
</script>
<Button onclick={openDialog}>Delete Account</Button>
<ConfirmInputDialog
bind:visible={showDialog}
title="Delete Account"
message="This action cannot be undone. Type 'DELETE' to confirm:"
confirmText="Delete"
cancelText="Cancel"
inputPlaceholder="Type DELETE"
requiredText="DELETE"
/>
Understanding the Basics
When building composite components with STDF, you're essentially creating wrapper components that:
- Combine multiple STDF components: Use Dialog, Popup, BottomSheet, Input, Button, etc. together
- Manage shared state: Coordinate state between multiple child components
- Handle events: Process and forward events from child components to parent
- Provide a simplified API: Hide complexity behind a cleaner, domain-specific interface
Key patterns:
-
State binding: Use
bind:directives to sync state between components - Event forwarding: Use Svelte's event system to communicate between components
-
Props spreading: Use
$$restPropsto pass additional props to underlying STDF components
Practical Example / Building Something Real
Let's build a more complex composite component: a FormDialog that combines Dialog, multiple Input fields, validation, and Button components to create a reusable form dialog pattern:
<!-- src/lib/components/FormDialog.svelte -->
<script>
import { Dialog, Input, Button, Cell, CellGroup } from 'stdf';
export let visible = $bindable(false);
export let title = 'Form Dialog';
export let submitText = 'Submit';
export let cancelText = 'Cancel';
export let fields = [];
export let onSubmit = () => {};
// Form state - dynamically created based on fields
let formData = {};
let errors = {};
let isSubmitting = false;
// Initialize form data from fields
$: {
if (fields.length > 0) {
formData = fields.reduce((acc, field) => {
acc[field.name] = field.value || '';
return acc;
}, {});
errors = {};
}
}
// Validation function
function validateField(name, value, rules) {
if (!rules) return true;
if (rules.required && !value) {
return rules.requiredMessage || `${name} is required`;
}
if (rules.pattern && !rules.pattern.test(value)) {
return rules.patternMessage || `${name} format is invalid`;
}
if (rules.minLength && value.length < rules.minLength) {
return `${name} must be at least ${rules.minLength} characters`;
}
if (rules.maxLength && value.length > rules.maxLength) {
return `${name} must be no more than ${rules.maxLength} characters`;
}
if (rules.custom && !rules.custom(value)) {
return rules.customMessage || `${name} validation failed`;
}
return null;
}
function handleFieldChange(name, value) {
formData[name] = value;
// Validate on change
const field = fields.find(f => f.name === name);
if (field?.rules) {
const error = validateField(name, value, field.rules);
if (error) {
errors[name] = error;
} else {
delete errors[name];
}
}
}
function handleSubmit() {
// Validate all fields
let hasErrors = false;
const newErrors = {};
fields.forEach(field => {
if (field.rules) {
const error = validateField(
formData[field.name],
formData[field.name],
field.rules
);
if (error) {
newErrors[field.name] = error;
hasErrors = true;
}
}
});
if (hasErrors) {
errors = newErrors;
return;
}
// Submit form
isSubmitting = true;
onSubmit(formData).then(() => {
isSubmitting = false;
visible = false;
// Reset form
formData = fields.reduce((acc, field) => {
acc[field.name] = field.value || '';
return acc;
}, {});
errors = {};
}).catch((error) => {
isSubmitting = false;
console.error('Form submission error:', error);
});
}
function handleCancel() {
visible = false;
// Reset form
formData = fields.reduce((acc, field) => {
acc[field.name] = field.value || '';
return acc;
}, {});
errors = {};
}
function handleClose() {
visible = false;
}
$: isValid = Object.keys(errors).length === 0 &&
fields.every(field => {
if (field.rules?.required) {
return formData[field.name]?.trim();
}
return true;
});
</script>
<Dialog
bind:visible
{title}
primaryText={submitText}
secondaryText={cancelText}
onprimary={handleSubmit}
onsecondary={handleCancel}
onclose={handleClose}
>
<div class="mt-4 space-y-4">
<CellGroup>
{#each fields as field}
<Cell>
<Input
bind:value={formData[field.name]}
type={field.type || 'text'}
placeholder={field.placeholder}
label={field.label}
required={field.rules?.required}
disabled={isSubmitting}
state={errors[field.name] ? 'error' : 'default'}
onchange={(e) => handleFieldChange(field.name, e.detail)}
/>
{#if errors[field.name]}
<p class="text-error text-sm mt-1 px-4">
{errors[field.name]}
</p>
{/if}
</Cell>
{/each}
</CellGroup>
</div>
</Dialog>
Now let's create a usage example with a user registration form:
<!-- src/routes/register/+page.svelte -->
<script>
import FormDialog from '$lib/components/FormDialog.svelte';
import { Button, Toast } from 'stdf';
let showForm = false;
const registrationFields = [
{
name: 'username',
label: 'Username',
placeholder: 'Enter username',
type: 'text',
rules: {
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/,
patternMessage: 'Username can only contain letters, numbers, and underscores'
}
},
{
name: 'email',
label: 'Email',
placeholder: 'Enter email address',
type: 'email',
rules: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
patternMessage: 'Please enter a valid email address'
}
},
{
name: 'password',
label: 'Password',
placeholder: 'Enter password',
type: 'password',
rules: {
required: true,
minLength: 8,
custom: (value) => /[A-Z]/.test(value) && /[a-z]/.test(value) && /[0-9]/.test(value),
customMessage: 'Password must contain uppercase, lowercase, and numbers'
}
},
{
name: 'confirmPassword',
label: 'Confirm Password',
placeholder: 'Confirm password',
type: 'password',
rules: {
required: true,
custom: (value, allData) => {
// Note: This would need access to formData, simplified here
return value === allData?.password;
},
customMessage: 'Passwords do not match'
}
}
];
async function handleSubmit(formData) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
Toast.show({
message: `Registration successful! Welcome, ${formData.username}`,
duration: 3000
});
console.log('Form submitted:', formData);
}
function openForm() {
showForm = true;
}
</script>
<div class="p-4">
<Button onclick={openForm} fill="base" state="theme" size="big">
Register New User
</Button>
</div>
<FormDialog
bind:visible={showForm}
title="User Registration"
submitText="Register"
cancelText="Cancel"
fields={registrationFields}
onSubmit={handleSubmit}
/>
Common Issues / Troubleshooting
Issue 1: Components Not Styling Correctly
Problem: STDF components appear unstyled or with incorrect colors.
Solution: Ensure your Tailwind CSS configuration includes the @source directive pointing to STDF components:
@source "../node_modules/stdf/**/*.svelte";
Also verify that your CSS file is imported in your app's entry point.
Issue 2: State Not Syncing Between Components
Problem: Changes in child components don't reflect in parent component state.
Solution: Use Svelte's $bindable() rune for two-way binding in Svelte 5:
export let visible = $bindable(false);
For Svelte 4, use bind:visible in the parent component.
Issue 3: Events Not Firing
Problem: Event handlers in composite components aren't being called.
Solution: Make sure you're using the correct event names. STDF components use specific event names like onprimary, onsecondary, onclose. Check the STDF documentation for the correct event names for each component.
Issue 4: Validation Not Working
Problem: Form validation doesn't trigger or show errors.
Solution: Ensure validation runs in reactive statements ($:) and that error state is properly managed. Also verify that field rules are correctly structured with the expected properties.
Next Steps
Now that you understand how to build composite components with STDF, consider exploring:
- Advanced state management: Using Svelte stores to share state across multiple composite components
- Component composition patterns: Building component libraries with slots, fragments, and dynamic components
-
Performance optimization: Using Svelte's
$derivedand$effectrunes for efficient reactivity - Accessibility: Adding ARIA attributes and keyboard navigation to composite components
- Testing: Writing unit tests for composite components using testing libraries
Check out other articles in this series for more STDF patterns and use cases.
Summary
This guide demonstrated how to build custom composite components with STDF by combining multiple STDF components into reusable, production-ready patterns. You learned how to create components that manage state, handle validation, and provide clean APIs for common UI patterns. You should now be able to build your own composite components that combine STDF's building blocks into more complex, domain-specific solutions.
Top comments (0)