Install Astro and shadcn-svelte following these instructions
Install the astro-superforms integration following these instructions
Install some components from shadcn-svelte
pnpm dlx shadcn-svelte@latest add card form input sonner
Create a validation schema and a type for your message
import { z } from "astro/zod";import type { ActionError } from "astro:actions"; export const zNewsletterValues = z.object({ email: z.string().email().min(1),}); export type NewsletterValues = z.infer<typeof zNewsletterValues>; export type Message = ActionError["code"] | "SUCCESS";
Create your action
import { zNewsletterValues } from "@/lib/utils";import { defineAsfAction } from "superforms:astro";import { message } from "sveltekit-superforms"; export const server = { //... subscribeToNewsletter: defineAsfAction({ input: zNewsletterValues, handler: async ({ email }, { form }) => { await new Promise((resolve) => setTimeout(resolve, 2000)); const response = Math.random(); if (response < 0.3) return message<Message>(form, "BAD_REQUEST", { status: 400 }); if (response < 0.5) return message<Message>(form, "CONFLICT", { status: 409 }); return message<Message>(form, "SUCCESS"); }, }), //...};
Initialize the form in your astro index page
---import "$lib/styles/app.css";import { superValidate } from "sveltekit-superforms";import { zod } from "sveltekit-superforms/adapters";import { zNewsletterValues } from "$lib/schemas";import Form from "./_form.svelte"; const sv = await superValidate(Astro.request, zod(zNewsletterValues));--- <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="viewport" content="width=device-width" /> <meta name="generator" content={Astro.generator} /> <title>Astro</title> </head> <body> <body class="flex items-center justify-center min-h-screen"> <Form client:load {sv} /> </body></html>
Display the form in your svelte component
<script lang="ts">import * as Card from "$lib/components/ui/card";import * as Form from "$lib/components/ui/form";import { Input } from "$lib/components/ui/input";import { Toaster } from "$lib/components/ui/sonner";import { zNewsletterValues, type Message, type NewsletterValues } from "$lib/schemas";import { actions } from "astro:actions";import { toast } from "svelte-sonner";import { superForm, type SuperValidated } from "sveltekit-superforms";import { zodClient } from "sveltekit-superforms/adapters"; let { sv }: { sv: SuperValidated<NewsletterValues, Message> } = $props(); const sf = superForm(sv, { validators: zodClient(zNewsletterValues), onError: () => { toast.error(i18n()); }, onUpdated: ({ form: { message, valid } }) => { if (message) valid ? toast.success(i18n(message)) : toast.error(i18n(message)); },});const { delayed, enhance, form, submitting } = sf; function i18n(code?: Message) { if (code === "BAD_REQUEST") return "An error occurred. Please try again later."; if (code === "CONFLICT") return "You are already subscribed."; if (code === "SUCCESS") return "Successfully subscribed."; return "An error occurred. Please try again later.";}</script> <Card.Root class="w-full max-w-md"> <form method="POST" action={actions.subscribeToNewsletter} use:enhance novalidate> <Card.Header> <Card.Title>Newsletter</Card.Title> <Card.Description>Please enter your email address to subscribe to our newsletter.</Card.Description> </Card.Header> <Card.Content> <Form.Field form={sf} name="email"> <Form.Control> {#snippet children({ props })} <Input {...props} type="email" bind:value={$form.email} /> {/snippet} </Form.Control> <Form.FieldErrors /> </Form.Field> </Card.Content> <Card.Footer> <Form.Button disabled={$submitting}>{$delayed ? "Subscribing..." : "Subscribe"}</Form.Button> </Card.Footer> </form></Card.Root><Toaster />