Standard helper to render buttons
Until now, every Desk feature that wanted an Espresso-style button hand-wrote the markup: a template literal with class="es-button", the right data-variant and data-size attributes, a manually bound click handler, and — if the author remembered — an escaped label, an aria-label for icon-only buttons, and some ad-hoc loading flag. Grep the codebase and each call site did a slightly different subset of that. Drift was the default.
What changed
PR #40576 by Nikhil Kothari adds frappe/public/js/frappe/ui/components/button.js: one helper, two forms, with a full JSDoc typedef so your editor autocompletes every option.
frappe.ui.button(opts)returns a wired jQuery element. Ifonclickreturns a promise, the button setsaria-busy, shows a spinner, and blocks clicks (including keyboard re-entry) until it settles.frappe.ui.button.html(opts)returns a markup string for template literals.- Labels, titles, classes, and attribute values are HTML-escaped;
on*attributes are refused outright, since entity decoding makes escaping useless there. - Icon without label renders a square icon-button and promotes
titletoaria-label; a missing title logs a console warning. - Loading state moved from
data-loadingtoaria-busy="true"— visual and accessibility state can no longer drift — with automatic loading labels for common verbs (Save becomes "Saving...") and a new pure-CSS.es-spinnercomponent.
frappe.ui.button({
label: __("Save"),
variant: "solid",
onclick: () => frm.save(), // promise → busy state handled for you
});
// string form, for template literals
`<div>${frappe.ui.button.html({ label: __("Load More") })}</div>`;
The PR also converts every existing es-button call site; a follow-up will migrate legacy btn markup.
> Return a promise from onclick and the loading state — spinner, aria-busy, click blocking — is handled for you. Stop hand-rolling busy flags.