Button Actions

Authors
Alexander Petros (contact@alexpetros.com)
Carson Gross (carson@bigsky.software)
Date Created
January 28, 2025
Last Updated
January 28, 2025
Issue Tracker
Pending
Status
Pending
Table of Contents

Summary

Give buttons the action attribute, allowing them to make HTTP requests.

Proposal 2/3 in the Triptych Proposals.

Goals

Proposed Changes

New attributes for the button:

When the action attribute is specified, clicking the button issues an HTTP request to the specified URL. The method attribute can be used to change the HTTP method of the request; it defaults to GET if not specified.

When a button with the action attribute is included inside a form, it does not submit the form. Instead, it makes the request without submitting additional data, exactly the same as if there had been no form at all.

Sample Usage

Logout Button

One of the most common user interactions on the web is logging in or out of a website. A simple POST form, with username and password inputs, can model this interaction.

Logouts, however, do not submit data. There is currently no semantic way to represent a logout button; you have to wrap it in a form. This is one way to do it:

<form action=/logout method=POST><button>Logout</button></form>

With support for button actions, we can instead model this as a lone button creating or deleting a "session" resource.Absent PUT, PATCH, and DELETE support, we can instead do <button action=/logout method=POST>Logout</button>, which is also fine.

<button action=/session method=DELETE>Logout</button>
This creates a simplifying symmetry for session management, and allows the logout action to take advantage of DELETE's idempotency. One benefit of this simplification is enabling server frameworks that implement file-based routing (like base PHP, SvelteKit, and NextJS) to implement their authentication entirely within one file. A JS codebase, for instance, might have a sessions.js file, which exports two methods: post and delete. That same file can contain internal helper functions that handle authentication, nicely encapsulating the session management logic and re-using many of the same resources.

Multi-Action Pages

Consider a simple job application webform, which uses button actions:

<form action="/apply" method="POST">
  <input type="text" name="name">
  <input type="email" name="email">
  <textarea name="coverletter"></textarea>

  <button>Submit</button>
  <button formaction="/draft">Save Draft</button>
  <button action="/" method="GET">Cancel</button>
</form>

The first part of the form is three inputs that the job seeker must fill out: a name, an email, and a cover letter.

The second part is three actions you can take: you can submit the application, save a draft of it, or give up on applying entirely. Each of these buttons interacts with the <form> element differently!

Semantically, each of these buttons represents a reasonable form operation: a user might choose to submit a form, save it as a draft, or cancel it entirely. Without button actions, you can't describe the "Cancel" action correctly. The button element's existing formmethod and formaction methods, which modify the behavior of their containing form, cannot achieve the same "Cancel" experience: using them here would incorrectly navigate to the home page with the form's contents as a query parameter.

You could achieve the same behavior with a link, however, and just style it like a button. That will be discussed further in the existing workarounds section.

Now let's say you save a draft and return to edit it. The web page looks like this:

<form action="/apply" method="POST">
  <input type="text" name="name" value="Alexander Petros">
  <input type="email" name="email" value="contact@example.com">
  <textarea name="coverletter">
    My name is Alex and my passion is enterprise software sales.
  </textarea>

  <button>Submit</button>
  <button formaction="/draft/123" formmethod="PUT">Save Draft</button>
  <button action="/draft/123" method="DELETE">Delete Draft</button>
  <button action="/" method="GET">Cancel</button>
</form>
While the inputs of this form are the same, they have been pre-filled with the current values of the draft job application. This is the essence of Hypertext As the Engine of Application State (HATEOAS). We have also changed one button and added a brand new one.

Once again, all of these buttons are, from the user's perspective, co-equal form controls. But only two of the buttons rely on the form data; two of them do not. Adding buttons that can perform requests independently not only removes the need to wrap them in forms, it also allows for simpler nesting within forms to achieve a complete set of form controls.

Technical Specification

Button actions are a seamless extension of the existing ways to make HTTP requests from HTML. They mimic the semantics of form submission, but with no data.

Enabling Button Actions

To make a request with a button, the action attribute must be specified with a valid URL. The method attribute defaults to GET if not specified; it can also be set to all supported HTTP form methods. At the time of this writing, only GET and POST are supported. With the adoption of Triptych #1, PUT, PATCH, and DELETE would also be supported. In the future, this attribute could include arbitrary custom methods as well.

Buttons can issue requests if they are type=submit, the default button type. submit is a mildly regrettable name in this context, as it is not typically used outside the context of a form (one does not really "submit" a link navigation). It is far more important, however, that the button actions work with the default type, which is and always will be submit, than it is to have a better name for that functionality. Besides, it's far from the most confusing default name in HTML. This is done to both simplify the interface, and align with author expectations that a button with type=submit (or no type specified) is capable of making requests. This document uses the term "author" in the same way that the HTML Design Principles do, to mean the person writing the HTML.

Buttons that have any other type attribute should ignore the method and action attributes.

Interaction with forms

Buttons with the action attribute that are children of a form will not submit any of the form data when clicked.

If the method attribute is specified but no action attribute is, then the method attribute is ignored; the existing formmethod attribute should be used to alter the method of the form.

Existing Workarounds

Wrap The Button in a Form

As seen in Section 3, buttons can currently be made to issue POST requests by wrapping them in a form:

<form action=/logout method=POST><button>Logout</button></form>

This wrapping obscures the intended semantics: that's just a button that makes a request, not a form to fill out. This limitation is not shared by JavaScript, where one can listen for click events on buttons and issue HTTP requests in response; forms play no role in that process. Wrapping buttons in input-less forms is a workaround for missing functionality in HTML.

Wrapping buttons in forms also only works for POST methods. One might be tempted to do the same with a GET request, like so:

<form action=/cancel method=GET><button>Cancel</button></form>

But this results in a request to /cancel?, with an extraneous query parameter, which is incorrect.

Nor this technique cannot be used within forms, for any method. Consider how you might implement the Multi-Action Pages example with currently-existing native HTML controls:Although still assuming PUT, PATCH, and DELETE support; absent those it's even less clear, because you have to use make both "Save Draft" and "Delete Draft" POSTs, and change the "Delete Draft" URL to something like /draft/123/delete.

<form action="/apply" method="POST">
  <input type="text" name="name" value="Alexander Petros">
  <input type="email" name="email" value="contact@example.com">
  <textarea name="coverletter">
    My name is Alex and my passion is enterprise software sales.
  </textarea>

  <button>Submit</button>
  <button formaction="/draft/123" formmethod="PUT">Save Draft</button>
  <button formmethod="DELETE">Delete Draft<button>
  <a href="/">Cancel</a>
</form>

Here the formmethod attribute has been set to DELETE so the browser (assuming DELETE is supported) will issue a DELETE request to /apply. Unfortunately, this request will include all of the values in the form, which is neither expected nor desired. Another drawback is that the form implementation of DELETE requests will likely use URL query parameters, like GET requests, rather than form-encoded bodies, like POST requests. This means that the URL may be excessively long, and contain leak data that should not have been shared at the URL level.

A second approach is to create a second form for the Delete Draft button. This approach suffers from the HTML specification rule that forms cannot be nested. Therefore we would need to put the new form outside the current form and refer to it using the form attribute:

<form action="/apply" method="POST">
  <input type="text" name="name" value="Alexander Petros">
  <input type="email" name="email" value="contact@example.com">
  <textarea name="coverletter">
    My name is Alex and my passion is enterprise software sales.
  </textarea>

  <button>Submit</button>
  <button formaction="/draft/123" formmethod="PUT">Save Draft</button>
  <button form="delete-form">Delete Draft<button>
  <a href="/">Cancel</a>
</form>

<form id="delete-form" action="/apply" method="DELETE"></form>

This gets the functionality working, but its meaning is much less clear to the HTML reader (both developers and assistive technologies) due to the loss of locality. For more on the topic of locality, and its importance to HTML specifically, see "Locality of Behaviour." The "Delete Draft" button has to be modeled as a separate form, even though logically it's not.

Note that in both examples, the Cancel button is now a link, which will have to be styled to look like a button, or else it will look very out of place. This despite the fact that "cancellation" is an action on the current resource, rather than a navigation to another resource (more on the implications of that in the following subsection).Styling links as buttons is also a somewhat controversial practice.

This limitation is endogenous to HTML, not the web platform itself: developers often eschew HTML functionality entirely by describing the UI with buttons and adding the appropriate functionality with scripting. Button actions remove this limitation, allowing HTML to model these form actions both semantically and functionally.

Using Links Instead of Buttons

For buttons that would issue GET requests, it is possible to instead use links and style them like a button. This is very common:

<a href=/logout class=button>Logout</a>
Visually, it is deceptively tricky to style links exactly like styled buttons. And, if you are using unstyled buttons that inherit the system button appearance from the user's OS (often the most accessible UX), then it is impossible.

Buttons and links also have different semantics. Buttons model actions which affect the current document, while hyperlinks model other documents (which the user could choose to open in the current browsing context, or a new one). Even though they both might result in GET requests, they represent different possibilities on the page.

These distinct semantics affect how browsers implement buttons and links. One example is the middle-click: when browsing with a mouse, you can middle-click a link and it will usually open in a new tab, but if you middle-click a form submission button, the form will get submitted in the same window.

Recall the Multi-Action Pages example, where one button submits the form and another cancels it.

<form action="/apply" method="POST">

  <!-- inputs omitted for clarity-->

  <button>Submit</button>
  <button action="/" method="GET">Cancel</button>
</form>

In a multi-page website, both of these actions should be page navigations. The "submit" button performs a page navigation with form submissions. The "close" button should be able to perform a page navigation that doesn't submit a form (since it was cancelled). A button enforces that the close action always happens in this browsing context, while a hyperlink does not.

This proposal does not take a stand on exactly when a button is appropriate versus a hyperlink. It simply acknowledges that there are times when a button is the appropriate semantic, even when the desired action is a page navigation. Those semantic differences are mildly consequential in our existing web browsing paradigms; they may be more or less so in future ones.

Alternatives

Disallow GET requests on buttons

Some might worry that allowing buttons to make GET requests could lead to confusion about when buttons are appropriate instead of hyperlinks. If this were a blocker to adoption, button actions could forgo implementing method=GET.

We are of the opinion that the hyperlink is the single best-understood HTML metaphor, and that it is very unlikely that authors will start defaulting to buttons where hyperlinks would be appropriate.

This also creates a confusing diversion from the form implementation, where method=GET is the default. Disallowing GET would require choosing a different for buttons, thereby creating unnecessary mental overhead in remembering which elements have which defaults for the same attribute.

Allow name and value

It's possible to let lone buttons submit a single datum, using the name and value attributes. htmx's hx-vals attribute is a more expansive take on this concept, where the values are encoded as JSON in the attribute value.
<button
  action=/members
  method=POST
  name=person
  value=Alex
  >Signup
</button>

This is a reasonable thing to add, and would fit well within the existing semantics of the button, which can have values that get submitted along with their parent form.

It is left out of the main proposal, however, in order to curtail scope. Most of the use-cases for this can also be accomplished with URLs and query parameters.

Footnotes