x-button is an accessible, themeable button web component implemented in pure ClojureScript with direct JavaScript interop and Custom Elements V1.
It is designed to be:
<button><x-button>Save</x-button>
x-button provides a reusable action control for user-triggered operations such as submit, reset, confirm, cancel, toggle, and destructive actions.
It supports:
The component is registered through the export namespace:
(ns app.exports.x-button
(:require [app.components.x-button.model :as model]
[app.components.x-button.x-button]))
(defn register!
[]
(app.components.x-button.x-button/init!))
(def public-api
{:tag-name model/tag-name
:properties model/property-api
:events model/event-schema
:observed-attributes model/observed-attributes})
(defn ^:export init
[]
(register!))
src/app/exports/x_button.cljs
src/app/components/x_button/x_button.cljs
src/app/components/x_button/model.cljs
If your compiled bundle exposes app.exports.x-button.init, register the element by calling:
app.exports.x_button.init();
Or ensure your compiled application calls the exported init function during startup.
<x-button>Save</x-button>
<x-button type="submit">Create account</x-button>
<x-button disabled>Unavailable</x-button>
<x-button loading label="Saving">
Saving
<span slot="spinner" aria-hidden="true"></span>
</x-button>
<x-button pressed>Bold</x-button>
<x-button label="Close">
<svg slot="icon-start" aria-hidden="true" viewBox="0 0 24 24"></svg>
</x-button>
<x-button>
<svg slot="icon-start" aria-hidden="true" viewBox="0 0 24 24"></svg>
Next
<svg slot="icon-end" aria-hidden="true" viewBox="0 0 24 24"></svg>
</x-button>
x-button
disabledBoolean attribute.
When present, the button is disabled and cannot be activated.
<x-button disabled>Disabled</x-button>
Behavior:
<button>loadingBoolean attribute.
When present, the button enters a busy/loading state and prevents duplicate activation.
<x-button loading label="Saving">
Save
<span slot="spinner" aria-hidden="true"></span>
</x-button>
Behavior:
<button>aria-busy="true"pressedBoolean attribute.
When present, the button is treated as pressed/toggled.
<x-button pressed>Bold</x-button>
Behavior:
aria-pressed="true"When absent:
aria-pressed="false"typeEnum attribute.
Allowed values:
buttonsubmitresetDefault:
button
Examples:
<x-button type="button">Open</x-button>
<x-button type="submit">Submit</x-button>
<x-button type="reset">Reset</x-button>
Invalid values normalize to button.
variantEnum attribute.
Allowed values:
primarysecondarytertiaryghostdangerDefault:
primary
Examples:
<x-button variant="primary">Save</x-button>
<x-button variant="secondary">Cancel</x-button>
<x-button variant="tertiary">Learn more</x-button>
<x-button variant="ghost">More</x-button>
<x-button variant="danger">Delete</x-button>
Invalid values normalize to primary.
sizeEnum attribute.
Allowed values:
smmdlgDefault:
md
Examples:
<x-button size="sm">Small</x-button>
<x-button size="md">Medium</x-button>
<x-button size="lg">Large</x-button>
Invalid values normalize to md.
labelString attribute.
Used as the accessible name fallback when the default slot does not contain meaningful text.
<x-button label="Close">
<svg slot="icon-start" aria-hidden="true"></svg>
</x-button>
Behavior:
label is not needed for naminglabel is used as aria-labellabel is present, the component is accessible-name invalidThese properties reflect boolean attributes.
disabledType:
boolean
Reflects:
disabled
Example:
const el = document.querySelector("x-button");
el.disabled = true;
loadingType:
boolean
Reflects:
loading
Example:
const el = document.querySelector("x-button");
el.loading = true;
pressedType:
boolean
Reflects:
pressed
Example:
const el = document.querySelector("x-button");
el.pressed = false;
Used for the button label/content.
<x-button>Save changes</x-button>
icon-startOptional leading icon slot.
<x-button>
<svg slot="icon-start" aria-hidden="true"></svg>
Download
</x-button>
icon-endOptional trailing icon slot.
<x-button>
Next
<svg slot="icon-end" aria-hidden="true"></svg>
</x-button>
spinnerOptional loading indicator slot.
<x-button loading label="Saving">
Save
<span slot="spinner" aria-hidden="true"></span>
</x-button>
By default, spinner content is treated as decorative and should usually be marked aria-hidden="true" unless explicitly intended to be announced.
All custom events:
x-button elementpressEmitted when activation succeeds.
Detail shape:
{ source: "pointer" | "keyboard" | "programmatic" }
Example:
button.addEventListener("press", (event) => {
console.log(event.detail.source);
});
Notes:
disabledloadingpress-startEmitted when a valid press interaction begins.
Detail shape:
{ source: "pointer" | "keyboard" }
Example:
button.addEventListener("press-start", (event) => {
console.log(event.detail.source);
});
press-endEmitted when a valid press interaction ends or is canceled.
Detail shape:
{ source: "pointer" | "keyboard" }
Example:
button.addEventListener("press-end", (event) => {
console.log(event.detail.source);
});
hover-startEmitted when an interactive pointer enters the button.
Detail shape:
{}
Example:
button.addEventListener("hover-start", () => {
console.log("hover start");
});
hover-endEmitted when an interactive pointer leaves the button.
Detail shape:
{}
Example:
button.addEventListener("hover-end", () => {
console.log("hover end");
});
focus-visibleEmitted when the internal native button enters visible keyboard focus state.
Detail shape:
{}
Example:
button.addEventListener("focus-visible", () => {
console.log("keyboard focus visible");
});
x-button uses a native internal <button> inside Shadow DOM.
This gives it:
type=submit and type=resetWhen disabled is present:
When loading is present:
aria-busy="true" is appliedWhen pressed is present:
aria-pressed="true" is setWhen pressed is absent:
aria-pressed="false" is setPreferred name sources:
label attribute as fallbackGood:
<x-button>Save</x-button>
Good:
<x-button label="Close">
<svg slot="icon-start" aria-hidden="true"></svg>
</x-button>
Invalid authoring:
<x-button></x-button>
Invalid authoring with only decorative content:
<x-button>
<svg slot="icon-start" aria-hidden="true"></svg>
</x-button>
Unless label is provided.
Because the internal control is a native button, it supports:
EnterSpaceThe component includes a visible focus style for keyboard-visible focus.
The spinner region is decorative by default. Authors should avoid allowing spinner visuals to replace the accessible name.
Recommended:
<x-button loading label="Saving">
Saving
<span slot="spinner" aria-hidden="true"></span>
</x-button>
The component is styled via semantic CSS custom properties defined on the host.
You can override these variables from outside the component.
--x-button-radius--x-button-gap--x-button-padding-inline--x-button-height-sm--x-button-height-md--x-button-height-lg--x-button-font-size-sm--x-button-font-size-md--x-button-font-size-lg--x-button-icon-size-sm--x-button-icon-size-md--x-button-icon-size-lg--x-button-spinner-size--x-button-bg--x-button-bg-hover--x-button-bg-active--x-button-bg-disabled--x-button-fg--x-button-fg-disabled--x-button-border--x-button-border-hover--x-button-border-active--x-button-focus-ring--x-button-danger-bg--x-button-danger-fg--x-button-transition-duration--x-button-transition-easingThe following shadow parts are exposed:
buttoninnerlabelicon-starticon-endspinnerExample:
x-button::part(button) {
font-weight: 600;
}
x-button::part(label) {
letter-spacing: 0.01em;
}
x-button.brand {
--x-button-bg: #2563eb;
--x-button-bg-hover: #1d4ed8;
--x-button-bg-active: #1e40af;
--x-button-focus-ring: #93c5fd;
}
<x-button class="brand">Continue</x-button>
The component provides default theme-aware values using prefers-color-scheme.
Host-level CSS variable overrides take precedence over defaults.
The component uses minimal CSS-based motion for:
It respects:
@media (prefers-reduced-motion: reduce)
In reduced motion environments, transitions are removed.
The internal native button honors the type attribute.
type="button"No form submission.
type="submit"Submits the nearest form.
type="reset"Resets the nearest form.
Example:
<form>
<input type="text" />
<x-button type="submit">Submit</x-button>
<x-button type="reset" variant="secondary">Reset</x-button>
</form>
typeInvalid or missing values normalize to:
button
variantInvalid or missing values normalize to:
primary
sizeInvalid or missing values normalize to:
md
Examples:
<x-button type="oops">Save</x-button>
<x-button variant="unknown">Save</x-button>
<x-button size="xl">Save</x-button>
These behave as if written:
<x-button type="button" variant="primary" size="md">Save</x-button>
The export namespace exposes:
(def public-api
{:tag-name model/tag-name
:properties model/property-api
:events model/event-schema
:observed-attributes model/observed-attributes})
(def property-api
{:disabled {:type 'boolean
:reflects-attribute attr-disabled}
:loading {:type 'boolean
:reflects-attribute attr-loading}
:pressed {:type 'boolean
:reflects-attribute attr-pressed}})
(def event-schema
{event-press {:detail {:source 'string}}
event-press-start {:detail {:source 'string}}
event-press-end {:detail {:source 'string}}
event-hover-start {:detail {}}
event-hover-end {:detail {}}
event-focus-visible {:detail {}}})
src/
app/
components/
x_button/
model.cljs
x_button.cljs
exports/
x_button.cljs
docs/
x_button.md
test/
app/
components/
x_button/
model_test.cljs
x_button_test.cljs
demo/
x_button.html
Recommended tests include:
loading disables the internal buttonpressed maps to aria-pressedlabel becomes aria-label when default slot lacks meaningful textpress emits expected detail<x-button id="save-btn" variant="primary">Save</x-button>
<script>
const btn = document.getElementById("save-btn");
btn.addEventListener("press", (event) => {
console.log("press", event.detail);
});
btn.addEventListener("press-start", (event) => {
console.log("press-start", event.detail);
});
btn.addEventListener("press-end", (event) => {
console.log("press-end", event.detail);
});
btn.addEventListener("hover-start", () => {
console.log("hover-start");
});
btn.addEventListener("hover-end", () => {
console.log("hover-end");
});
btn.addEventListener("focus-visible", () => {
console.log("focus-visible");
});
</script>
Best:
<x-button>Save</x-button>
label for icon-only casesBest:
<x-button label="Close">
<svg slot="icon-start" aria-hidden="true"></svg>
</x-button>
Recommended:
<svg slot="icon-start" aria-hidden="true"></svg>
Recommended:
<span slot="spinner" aria-hidden="true"></span>
Avoid:
<x-button></x-button>
x-button does not include:
x-button is a platform-native button component that provides:
disabled, loading, and pressedtype, variant, and sizepublic-apiIt is intended to be a simple, robust, Closure Advanced safe foundation for action controls in a ClojureScript design system.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |