How to Integrate Poketto API with SvelteKit (Svelte 5) and Tailwind CSS
This tutorial will guide you through integrating Poketto's form API into a SvelteKit project using Svelte 5's new runes syntax and Tailwind CSS for styling.
Prerequisites
- Node.js 18+ installed
- A Poketto account with API access
- Basic knowledge of SvelteKit and Svelte
Project Setup
1. Create a New SvelteKit Project
Read more here
2. Install Tailwind CSS
Read more here
3. Environment Variables
Create a .env.development
file in your project root:
POKETTO_API_KEY=your_poketto_api_key_here
POKETTO_FORM_ID=your_form_id_here // optional, you can add the form id directly in the component
Backend Implementation
4. Create the API Route
Create src/routes/api/poketto/+server.ts
:
import { json, type RequestHandler } from '@sveltejs/kit';
import { POKETTO_API_KEY, POKETTO_FORM_ID } from '$env/static/private';
export const POST: RequestHandler = async ({ request }) => {
const { field_responses, respondent } = await request.json();
console.log('Poketto form submission:', {
field_responses,
respondent
});
try {
const pokettoResponse = await fetch(
`https://api.poketto.dev/api/v1/forms/submit/\${POKETTO_FORM_ID}`,
{
method: 'POST',
headers: {
'X-API-Key': POKETTO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
respondent,
field_responses
})
}
);
if (!pokettoResponse.ok) {
throw new Error(`Poketto API error: \${pokettoResponse.status}`);
}
const pokettoData = await pokettoResponse.json();
return json({
success: true,
poketto_response: pokettoData
});
} catch (error) {
console.error('Error submitting to Poketto:', error);
return json(
{
success: false,
error: 'Failed to submit to Poketto API'
},
{ status: 500 }
);
}
};
Frontend Implementation
5. Create the Feedback Widget Component
Create src/lib/components/FeedbackWidget.svelte
:
<script lang="ts">
import { onMount } from 'svelte';
let feedback = $state('');
let selectedEmoji = $state('');
let isOpen = $state(false);
let isSubmitting = $state(false);
let submitError = $state('');
let submitSuccess = $state(false);
let userEmail = $state('');
let showEmail = $state(false);
let emailError = $state('');
const emojis = ['🤩', '🙂', '🙁', '😭'];
async function getUserData() {
try {
} catch (error) {
}
}
async function submitFeedback() {
if (!selectedEmoji) {
submitError = 'Please select an emoji rating';
return;
}
isSubmitting = true;
submitError = '';
submitSuccess = false;
try {
const respondentData = {
email: userEmail || '',
browser: userData.browser || '',
os: userData.os || '',
device_type: userData.device_type || '',
screen_resolution: userData.screen_resolution || '',
referrer_url: userData.referrer_url || '',
country: userData.country || '',
user_agent: userData.user_agent || '',
language: userData.language || '',
timezone: userData.timezone || '',
};
const emojiIndex = emojis.length - 1 - emojis.indexOf(selectedEmoji);
const fieldResponsesArray = [
{
field_id: '51b91f2e',
value: feedback
},
{
field_id: 'c6bbfee9',
value: emojiIndex.toString()
},
{
field_id: '79dd0efa',
value: userEmail || ''
}
];
const requestData = {
respondent: respondentData,
field_responses: fieldResponsesArray
};
console.log('Submitting to Poketto API...');
const response = await fetch('/api/poketto', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
}
const responseData = await response.json();
console.log('Form submission successful:', responseData);
submitSuccess = true;
setTimeout(() => {
feedback = '';
selectedEmoji = '';
userEmail = '';
showEmail = false;
isOpen = false;
submitSuccess = false;
}, 2000);
} catch (error) {
console.error('Error submitting form:', error);
submitError =
error instanceof Error ? error.message : 'Failed to submit form. Please try again.';
} finally {
isSubmitting = false;
}
}
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validateEmail() {
if (userEmail && !isValidEmail(userEmail)) {
emailError = 'Invalid email format';
} else {
emailError = '';
}
}
</script>
<!-- Feedback Widget -->
<div class="fixed bottom-4 right-4 z-50">
{#if !isOpen}
<button
onclick={() => (isOpen = true)}
class="bg-blue-600 hover:bg-blue-700 text-white rounded-full p-3 shadow-lg transition-colors duration-200"
aria-label="Open feedback form"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
</button>
{:else}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-80 max-w-sm">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Send Feedback</h3>
<button
onclick={() => (isOpen = false)}
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
aria-label="Close feedback form"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
{#if submitSuccess}
<div class="text-center py-8">
<div class="text-green-500 text-4xl mb-2">✓</div>
<p class="text-green-600 dark:text-green-400 font-medium">Thank you for your feedback!</p>
</div>
{:else}
<div class="space-y-4">
<div>
<textarea
bind:value={feedback}
placeholder="Tell us what you think..."
rows="4"
maxlength="1024"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white resize-none"
></textarea>
</div>
<!-- Email Input (Optional) -->
{#if showEmail}
<div>
<input
bind:value={userEmail}
oninput={validateEmail}
type="email"
placeholder="Your email (optional)"
maxlength="128"
class="w-full px-3 py-2 border {emailError
? 'border-red-500'
: 'border-gray-300 dark:border-gray-600'} rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/>
{#if emailError}
<p class="text-red-500 text-sm mt-1">{emailError}</p>
{/if}
</div>
{:else}
<button
onclick={() => (showEmail = true)}
class="text-blue-600 dark:text-blue-400 text-sm hover:underline flex items-center gap-1"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
Add email
</button>
{/if}
<div class="flex items-center justify-between">
<div class="flex gap-2">
{#each emojis as emoji}
<label class="cursor-pointer">
<input
type="radio"
name="emoji-rating"
value={emoji}
bind:group={selectedEmoji}
class="sr-only"
/>
<span
class="text-2xl transition-transform hover:scale-110 {selectedEmoji === emoji
? 'scale-125 opacity-100'
: 'opacity-70'}"
>
{emoji}
</span>
</label>
{/each}
</div>
<button
onclick={submitFeedback}
disabled={isSubmitting || (!selectedEmoji && !feedback.trim())}
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Sending...' : 'Send'}
</button>
</div>
{#if submitError}
<div
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3"
>
<p class="text-red-600 dark:text-red-400 text-sm">{submitError}</p>
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
6. Use the Component in Your App
Update src/routes/+layout.svelte
:
<script>
import '../app.css';
import FeedbackWidget from '$lib/components/FeedbackWidget.svelte';
let { children } = $props();
</script>
<FeedbackWidget />
<main>
{@render children()}
</main>
Configuration
7. Poketto Form Setup
Log into your Poketto dashboard
Create a new form with the following fields:
- Feedback Text (field_id:
51b91f2e
)
- Emoji Rating (field_id:
c6bbfee9
)
- Email (field_id:
79dd0efa
)
Replace the field IDs in the component with your actual field IDs
Update your .env
file with your form ID and API key
8. Customization Options
Styling
The component uses Tailwind CSS classes. You can customize:
- Colors by changing
bg-blue-600
to your brand colors
- Positioning by modifying
fixed bottom-4 right-4
- Size by adjusting
w-80 max-w-sm
Data Collection
You can extend the getUserData()
function to collect additional analytics:
- Custom user properties
- Page-specific data
- Session information
- Custom UTM parameters
Form Fields
Add or remove form fields by modifying the fieldResponsesArray
in the submitFeedback()
function.
Testing
9. Run Your Application
Visit http://localhost:5173
and test the feedback widget:
- Click the feedback button in the bottom-right corner
- Fill out the form with test data
- Submit and verify the data appears in your Poketto dashboard
Best Practices
Error Handling
- Always validate user input before submission
- Provide clear error messages
- Handle network failures gracefully
Privacy
- Make email collection optional
- Inform users about data collection
- Respect user privacy preferences
Performance
- Lazy load the user data collection
- Debounce form submissions
- Cache user data when appropriate
Accessibility
- Use proper ARIA labels
- Ensure keyboard navigation works
- Provide screen reader support
Conclusion
You now have a fully functional feedback widget integrated with Poketto API in your SvelteKit application. The widget collects comprehensive user data and analytics while providing a smooth user experience with Svelte 5's reactive system and Tailwind CSS styling.