Progressive Enhancement

Works Without JS — Faster With It

Progressive Enhancement

Server Actions on `<form action>` submit via a real HTML POST even with JS disabled. With JS enabled, Next intercepts and updates in place — same UX, better experience.

4 min read Level 2/5 #nextjs#forms#progressive-enhancement
What you'll learn
  • Build forms that work without JS
  • Avoid `useState` for fields you do not need to control
  • Test the no-JS path explicitly

A core promise of Server Actions is that the same form works without JavaScript. If the hydration script fails, the network is slow, or a user has JS disabled — the form still submits via a real browser POST.

What Makes a Form Progressive

Use plain HTML form elements with name attributes. Do not gate submission on JS state. The browser already knows how to serialize and POST a form; let it.

import { createPost } from './actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <label>
        Title <input name="title" required minLength={3} />
      </label>
      <label>
        Body <textarea name="body" required />
      </label>
      <button>Publish</button>
    </form>
  )
}

No useState, no onSubmit, no 'use client'. The required and minLength attributes even give you HTML-native validation for free.

What JS Adds

With hydration in place, Next.js intercepts the submit. Instead of a full page reload, the action runs, the result streams in, and revalidation happens — all without losing scroll position or focus.

'use client'
// Drop-in upgrades when you opt in
import { useActionState } from 'react'
const [state, action, pending] = useActionState(createPost, {})

The exact same <form> markup now gets inline error rendering and pending state.

Testing Without JS

In Chrome DevTools: open the command menu (Cmd/Ctrl+Shift+P), run “Disable JavaScript”, reload. Submitting should still work — you will get a full reload, but the data lands.

If your form breaks without JS, you have accidentally coupled it to client state. Most often the fix is removing an unnecessary 'use client' and letting the form be a plain <form action={action}>.

Mutations & Revalidation →