Button
<ol-button>
Variants
Secondary is the default — a bare <ol-button> renders as secondary.
<ol-button>
Secondary
</ol-button>
<ol-button variant="primary">
Primary
</ol-button>
<ol-button variant="destructive">
Destructive
</ol-button>
<ol-button variant="ghost">
Ghost
</ol-button>
Links
Set href and the component renders an <a> with the same styling — for button-shaped navigation like Read, Borrow, or Find in a library. disabled and loading drop the href and set aria-disabled.
<ol-button variant="primary" href="/search?q=hobbit">
Read
</ol-button>
<ol-button href="/search?q=hobbit">
Borrow
</ol-button>
<ol-button href="/search?q=hobbit" target="_blank" rel="noopener">
Find in a library
</ol-button>
<ol-button href="/search?q=hobbit" disabled>
Unavailable
</ol-button>
Icon-only shapes
shape="icon" makes a square whose side matches the size's control height; shape="circle" rounds it. Always give these an aria-label.
<ol-button shape="icon" aria-label="Search">
<svg ...></svg>
</ol-button>
<ol-button shape="circle" size="small" aria-label="Save">
<svg ...></svg>
</ol-button>
Floating
elevation="floating" swaps in a heavier drop shadow for a control that sits over content — a save button on cover art, a scroll-to-top button — instead of on the page surface.
<ol-button shape="circle" elevation="floating" aria-label="Save">
<svg width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true">
<path d="M12 5v14M5 12h14" />
</svg>
</ol-button>
<ol-button elevation="floating">
Back to top
</ol-button>
Sizes
Heights come from the shared --control-height-* tokens, so each size lines up with the same-size segmented control and input.
<ol-button size="small">
Small
</ol-button>
<ol-button>
Medium
</ol-button>
<ol-button size="large">
Large
</ol-button>
With icon
Put an inline SVG in the icon-start or icon-end slot. The button sizes it to match (14/16/18px by size) and adds the gap, so the SVG needs no width, height, or margin of its own.
<ol-button size="small">
<svg slot="icon-start" ...></svg>
Preview
</ol-button>
<ol-button>
Next
<svg slot="icon-end" ...></svg>
</ol-button>
Disabled
<ol-button disabled>
Secondary
</ol-button>
<ol-button variant="primary" disabled>
Primary
</ol-button>
<ol-button variant="destructive" disabled>
Destructive
</ol-button>
Loading
Shows a spinner and blocks interaction. The label stays in the DOM so the button's width doesn't shift.
<ol-button variant="primary" loading>
Save
</ol-button>
<ol-button loading>
Cancel
</ol-button>
<ol-button variant="destructive" loading>
Delete
</ol-button>
Loading, driven from JS
Click for a 1.5s fake request. Setting .loading disables the button for the duration.
button.loading = true;
await save();
button.loading = false;
Full width
<ol-button full-width>
Continue
</ol-button>
Form submission
The element is form-associated and keeps a hidden native submit button in its light DOM, so it behaves like a native button: type="submit" submits the enclosing form (native validation runs, Enter in a text field submits, event.submitter is set, name/value are submitted, formaction and friends work), type="reset" resets it, and preventDefault() on a click cancels either. A disabled fieldset disables it too.
Last submitted: — (submits: )
<form id="demo-button-form">
<fieldset id="demo-button-form-fieldset" class="ds-demo-row">
<label for="demo-button-form-input">Your name:</label>
<input id="demo-button-form-input" name="name" required>
<label for="demo-button-form-input-2">Nickname:</label>
<input id="demo-button-form-input-2" name="nickname">
<ol-button type="submit" name="action" value="save">
Submit
</ol-button>
<ol-button type="reset" variant="secondary">
Reset
</ol-button>
</fieldset>
</form>
<p>
<ol-toggle id="demo-button-form-disable" variant="button">
Disable fieldset
</ol-toggle>
</p>
<p>Last submitted: <strong id="demo-button-form-output">—</strong> (submits: <span id="demo-button-form-count">0</span>)</p>
<script>
(function () {
const form = document.getElementById('demo-button-form');
const out = document.getElementById('demo-button-form-output');
const count = document.getElementById('demo-button-form-count');
const fieldset = document.getElementById('demo-button-form-fieldset');
let submits = 0;
form.addEventListener('submit', function (e) {
e.preventDefault();
count.textContent = ++submits;
// Pass the submitter so its name/value are included, as they would be on a real submission.
const data = new FormData(form, e.submitter);
const via = e.submitter ? e.submitter.closest('ol-button').textContent.trim() : 'no submitter';
out.textContent = (data.get('name') || '—') + ' [' + data.get('action') + ', via ' + via + ']';
});
form.addEventListener('reset', function () { out.textContent = '—'; });
document.getElementById('demo-button-form-disable').addEventListener('ol-toggle-change', function (e) {
fieldset.disabled = e.detail.checked;
});
})();
</script>
Styling from outside
Two parts are exposed: control (the inner button or link) and label (the span around the slotted content). Use them for layout tweaks a consumer legitimately owns — like clamping a long label — never to restyle the button itself.
.filter-trigger ol-button::part(label) {
max-width: 12ch;
overflow: hidden;
text-overflow: ellipsis;
}
Events. None custom — use the native click, which bubbles from the inner <button>.
Rendering. Shadow DOM. The component renders and paints a real <button> (or <a>) in its shadow root, so it works inside any other component's shadow root too. Before upgrade the host tag is styled by components/ol-button.css (ol-button:not(:defined), loaded render-blocking site-wide) so server-rendered buttons look right on first paint. Style from outside via ::part(control) and ::part(label), never by reaching for the inner element.
API reference
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
variant
|
variant
|
"primary" | "secondary" | "destructive" | "ghost"
|
'secondary'
|
Default: "secondary". Ghost is transparent with no border or lift; it fills on hover. |
size
|
size
|
"small" | "medium" | "large"
|
'medium'
|
Default: "medium" |
type
|
type
|
"button" | "submit" | "reset"
|
'button'
|
Default: "button" |
loading
|
loading
|
Boolean
|
false
|
Shows a spinner and disables interaction. |
disabled
|
disabled
|
Boolean
|
false
|
Disables interaction. |
fullWidth
|
full-width
|
Boolean
|
false
|
Button expands to fill its container. |
shape
|
shape
|
"icon" | "circle"
|
— | Icon-only: width equals the size's height, no horizontal padding. "circle" additionally rounds it. Give it an aria-label. |
elevation
|
elevation
|
"floating"
|
— | Heavier drop shadow for a control that sits over content (e.g. a save button on cover art) rather than on the page. |
name
|
name
|
string
|
— | Submitted with `value` when this button submits the form. |
value
|
value
|
String
|
— | See `name`. |
href
|
href
|
String
|
— | Renders an <a> instead of a <button>. |
target
|
target
|
String
|
— | Link target (only with href). |
rel
|
rel
|
String
|
— | Link rel (only with href). |
download
|
download
|
String
|
— | Link download attribute (only with href). |
isDisabled
|
— | — | — | Whether the control is disabled from either source: its own `disabled` property or an ancestor `<fieldset disabled>`. Use this — not `disabled` — to gate interaction and to set `?disabled` on inner controls. |
formAssociatedValue
|
— | — | — | Override point. The value(s) to submit with the form. |
Slots
| Slot | Description |
|---|---|
| (default) | Default slot carries the button label. |
icon-start
|
Leading icon (an inline SVG). Sized to the button's size (14/16/18px) and separated from the label by a 4px gap only when filled. |
icon-end
|
Trailing icon, same treatment. Not for the disclosure chevron, which is automatic on popover triggers. |
CSS parts
| Part | Description |
|---|---|
control
|
The inner <button> or <a>. |
label
|
The span wrapping the slotted label. |