HTMX in Action How to Build a Lightweight Signup Form

HTMX in Action: How to Build a Lightweight Signup Form

Chris Sonnenberg HTML5, Tutorial Leave a Comment

Creating dynamic, responsive web forms doesn’t have to be complicated. With HTMX, you can build a seamless signup form using just a few HTML enhancements. In this blog, I’m going to show how to make a basic signup form with HTMX.

We’ll cover how a few minor improvements to HTML allow us to make a simple form quickly while keeping state consistent. There will be only three sections to the form, but it’s easily extendable with changes in only one or two routes per change.

What Is HTMX?

HTMX extends HTML by adding attributes that enable HTTP requests from any element—not just forms or links. This expands your options beyond GET and POST methods, allowing you to use all HTTP methods. It also lets you update targeted parts of the DOM rather than reloading the entire page. This results in more declarative and maintainable HTML.

Now that we’ve covered what HTMX is and how it enhances HTML, let’s dive into building our signup form. We’ll break the process into three simple steps: collecting basic information, selecting approved products, and confirming the signup. Each step will demonstrate how HTMX keeps things clean and efficient.

First Page: Collecting Basic Info

Our first page will use a form to gather some basic information: name and wealth. This form leverages HTMX’s hx-post attribute to send a POST request to our /initial endpoint when the user clicks Submit. The response will replace the entire form, creating a smooth, seamless experience.
By handling everything within a single page and updating only the necessary content, this approach mimics the developer experience of a modern single-page application — without the added complexity of a frontend framework.

<form hx-post="/initial" hx-swap="outerHTML">
  <label for="name">Name:</label>
  <input id="name" name="name" type="text" />
  <label for="wealth">Wealth:</label>
  <input id="wealth" name="wealth" type="number" value="0.00" step="0.01" />
  <button type="submit">Submit</button>
</form>

Second Page: Selecting Approved Products

After the initial form submission, our server will:

  1. Perform basic validation.
  2. Build a list of approved products based on the customer’s provided wealth.
  3. Save the customer information.
  4. Return a new form for product selection.
Related Posts:  Navigating the High Seas of CSS Anchor Positioning

By using HTMX, we keep all state management in the backend. This will minimize complexity and reduce the need for frontend caching or state logic often required by modern frameworks.

let products = [];
if(wealth > 0) products.push('checking');
if(wealth > 100) products.push('savings');
if(wealth > 10000) products.push('investing');
customers.push({ id: newid, name, wealth, products });
    
let html = ['<html><body>'];
html.push('<form hx-post="/products" hx-swap="outerHTML">');
html.push(`<p>${name}, which products do you want?</p>`);
html.push(`<input hidden name="id" value="${newid}" />`);
for(let i = 0; i < products.length; i++) {
  const p = products[i];
  html.push(`<input id="${p}-${i}" type="checkbox" name="${p}" />`);
  html.push(`<label for="${p}-${i}">${p}</label>`);
  html.push('<br />');
}
html.push('<button type="submit">Submit</button>');
html.push('</form>');
html.push('</body></html>');
res.send(html.join('\n'));

Final Page: Confirming the Sign-Up

The /products endpoint will save the customer’s chosen products and return a simple “Thank You” message. While there’s not much to display at this stage, this is an ideal place to implement additional actions like:

  • Sending a confirmation email to the customer.
  • Notifying internal teams or employees.
  • Triggering follow-up workflows for onboarding or customer engagement.

By keeping this logic server-side and leveraging HTMX’s lightweight updates, you maintain a streamlined, maintainable architecture.

Conclusion

In this guide, we’ve demonstrated how HTMX can simplify the process of building a multi-step signup form using just a few declarative attributes and minimal server-side rendering. By leveraging HTMX, we achieved a smooth, single-page experience without the complexity of a full frontend framework.

This lightweight approach keeps your HTML clean, your backend logic centralized, and your application easier to maintain — all while delivering a seamless user experience.

If you’re looking for a simple yet powerful way to enhance your web applications, HTMX is an excellent tool to explore. And if you’re looking for other ways to enhance your applications, the Keyhole Dev Blog is a great place to start. Thanks for reading!

Related Posts:  Navigating the High Seas of CSS Anchor Positioning

Complete Code

const express = require('express'),
  body_parser = require('body-parser'),
  app = express();

app.set('etag', false);
app.use(express.static('public'));
app.use(body_parser.urlencoded({ extended: false }));

function makeIds() {
  let n = 0;
  return () => n++;
}
const customerIds = makeIds();
let customers = [];

app.get('/', (req, res) => {
  res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>My Business</title>
  <meta name="description" content="index" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="color-scheme" content="dark" />
  <script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
  <h1>Sign Up Form</h1>
  <p>Please enter your information for a quote</p>
  <form hx-post="/initial" hx-swap="outerHTML">
    <label for="name">Name:</label>
    <input id="name" name="name" type="text" />
    <label for="wealth">Wealth:</label>
    <input id="wealth" name="wealth" type="number" value="0.00" step="0.01" />
    <button type="submit">Submit</button>
  </form>
  <hr />
  <h2>Customers</h2>
  <button hx-get="/customers" hx-target="#customer-list">Update</button>
  <div id='customer-list'></div>
</body>
</html>`);
});

app.get('/customers', (req, res) => {
  let html = ['<html><body>'];
  html.push('<ul>');
  for(let i = 0; i < customers.length; i++) {
    const c = customers[i];
    html.push(
      `<li>${c.name} - ${c.wealth}` +
      `- ${c.products?.join(',') ?? 'none'} - ` +
      `${c.chosen?.join(',') ?? 'none'}</li>`);
  }
  html.push('</ul>');
  html.push('</body></html>');
  res.send(html.join('\n'));
});

app.post('/initial', (req, res) => {
  const name = req.body.name,
    wealth = parseFloat(req.body.wealth),
    newid = customerIds();
  if(wealth <= 0) {
    customers.push({ id: newid, name, wealth });
    return res.send(`
<html>
<body>
  <p>You don't qualify for any of our products.</p>
  <a href="/">Home</a>
</body>
</html>`);
  }

  let products = [];
  if(wealth > 0) products.push('checking');
  if(wealth > 100) products.push('savings');
  if(wealth > 10000) products.push('investing');
  customers.push({ id: newid, name, wealth, products });
    
  let html = ['<html><body>'];
  html.push('<form hx-post="/products" hx-swap="outerHTML">');
  html.push(`<p>${name}, which products do you want?</p>`);
  html.push(`<input hidden name="id" value="${newid}" />`);
  for(let i = 0; i < products.length; i++) {
    const p = products[i];
    html.push(`<input id="${p}-${i}" type="checkbox" name="${p}" />`);
    html.push(`<label for="${p}-${i}">${p}</label>`);
    html.push('<br />');
  }
  html.push('<button type="submit">Submit</button>');
  html.push('</form>');
  html.push('</body></html>');
  res.send(html.join('\n'));
});

app.post('/products', (req, res) => {
  const id = parseInt(req.body.id),
    chosechecking = req.body.checking === 'on',
    chosesavings = req.body.savings === 'on',
    choseinvesting = req.body.investing === 'on';
  for(let i = 0; i < customers.length; i++) {
    if(customers[i].id !== id) continue;
    customers[i].chosen = [];
    if(chosechecking) customers[i].chosen.push('checking');
    if(chosesavings) customers[i].chosen.push('savings');
    if(choseinvesting) customers[i].chosen.push('investing');
    break;
  }
  res.send(`
<html>
<body>
  <p>Thank you for signing up</p>
  <a href="/">Home</a>
</body>
</html>`);
});

app.listen(8080, () => {
  console.log('waiting for connection http://localhost:8080/');
});
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments