Form with progressive enhancement

<Form name="X"> renders a plain HTML form targeting /_bext/action/X. Without JS it submits normally and the server 303s back. With FORM_CLIENT_RUNTIME on the page, submissions go through fetch with x-bext-form: 1 — the server returns the action's JSON, and a bext:result CustomEvent fires on the form. Same form, two paths.

Tip
The form works without JavaScript: without the runtime the browser submits normally and the action returns a 303 redirect. FORM_CLIENT_RUNTIME inlines only ~400 B of vanilla JS and never changes the form markup — no extra data-* attributes needed.

Try it

idle

src/actions/echoFormDemo.tsTypeScript
// src/actions/echoFormDemo.ts — server action returning JSON
"use server";
export async function echoFormDemo(req: Request): Promise<Response> {
  const form = await req.formData();
  return Response.json({ greeting: "hello, " + form.get("name"), at: new Date().toISOString() });
}

// page.tsx — Form component + client runtime
import { Form, FORM_CLIENT_RUNTIME } from "@bext-stack/framework/form";

// Same form works with or without JS:
// - no JS → standard POST 303 redirect
// - JS on → fetch with x-bext-form:1, server returns JSON, bext:result fires
<Form name="echoFormDemo">
  <input name="name" required />
  <button type="submit">greet</button>
</Form>

// Include the runtime once per page (inlines ~400 B of vanilla JS)
<Raw html={FORM_CLIENT_RUNTIME} />

<script>
  document.addEventListener("bext:pending", e => { /* show spinner */ });
  document.addEventListener("bext:result", e => {
    // e.detail.result is the JSON the action returned
    document.getElementById("out").textContent = e.detail.result.greeting;
  });
</script>