Button

ui-button wraps a native <button> in its shadow root and adds visual variants and sizes. It is form-associated. With type="submit" or type="reset" it submits or resets the form it sits in, even though the inner button lives in a shadow root. It also forwards the native invoker attributes popovertarget, commandfor and command, so it can open popovers and dialogs without script.

Unlike a native <button>, the default type is button, so a ui-button inside a form does nothing until you set type="submit" or type="reset".

Previewindex.html
Button
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button>Button</ui-button>
</body>
</html>

Examples

Variants

Set variant to default, destructive, outline, secondary, ghost or link.

Previewindex.html
Default Secondary Destructive Outline Ghost Link
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button>Default</ui-button>
  <ui-button variant="secondary">Secondary</ui-button>
  <ui-button variant="destructive">Destructive</ui-button>
  <ui-button variant="outline">Outline</ui-button>
  <ui-button variant="ghost">Ghost</ui-button>
  <ui-button variant="link">Link</ui-button>
</body>
</html>

Sizes

Text buttons come in xs, sm, default and lg.

Previewindex.html
Extra small Small Default Large
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button size="xs">Extra small</ui-button>
  <ui-button size="sm">Small</ui-button>
  <ui-button>Default</ui-button>
  <ui-button size="lg">Large</ui-button>
</body>
</html>

Icon buttons

The icon, icon-xs, icon-sm and icon-lg sizes make a square button. A slotted <svg> is sized to the button text. Give icon-only buttons an accessible name with aria-label.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button variant="outline" size="icon-xs" aria-label="Add">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
  </ui-button>
  <ui-button variant="outline" size="icon-sm" aria-label="Add">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
  </ui-button>
  <ui-button variant="outline" size="icon" aria-label="Add">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
  </ui-button>
  <ui-button variant="outline" size="icon-lg" aria-label="Add">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
  </ui-button>
</body>
</html>

With an icon

Put an <svg> before or after the label. The button adds a gap between them.

Previewindex.html
Upload Next
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button>
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 12v8h16v-8M12 3v12M7 8l5-5 5 5" /></svg>
    Upload
  </ui-button>
  <ui-button variant="outline">
    Next
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg>
  </ui-button>
</body>
</html>

Loading

Combine disabled with a ui-spinner while work is in progress.

Previewindex.html
Saving
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/spinner.html"></import-element>

  <ui-button disabled><ui-spinner size="14"></ui-spinner> Saving</ui-button>
</body>
</html>

Disabled

disabled disables the inner button and reflects to the host, so ui-button[disabled] can be styled from outside.

Previewindex.html
Disabled Disabled
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button disabled>Disabled</ui-button>
  <ui-button variant="outline" disabled>Disabled</ui-button>
</body>
</html>

In a form

type="submit" submits the surrounding form and type="reset" resets it. Buttons inside a disabled <fieldset> are disabled too.

Previewindex.htmlbutton-form-demo.html
Save Reset
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>

  <import-element from="./button-form-demo.html"></import-element>
  <button-form-demo></button-form-demo>
</body>
</html>
<custom-element name="button-form-demo">
  <script>
    const result = useState({ text: "Submit the form to see its data." });

    function submit(event) {
      event.preventDefault();
      const entries = [...new FormData(event.target)].map(([name, value]) => `${name}=${value}`);
      result.text = entries.join("\n") || "(no fields)";
    }

    function reset() {
      result.text = "The form was reset.";
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 4);
      width: 100%;
      max-width: calc(var(--spacing) * 80);
    }
    form {
      display: grid;
      gap: calc(var(--spacing) * 3);
    }
    .actions {
      display: flex;
      gap: calc(var(--spacing) * 2);
    }
    output {
      display: block;
      padding: calc(var(--spacing) * 3);
      border-radius: var(--radius-md);
      background: var(--muted);
      font-family: var(--font-mono);
      font-size: var(--text-xs);
      white-space: pre-wrap;
    }
  </style>

  <form onsubmit="submit(event)" onreset="reset()">
    <ui-input name="title" value="Quarterly report" aria-label="Title"></ui-input>
    <div class="actions">
      <ui-button type="submit">Save</ui-button>
      <ui-button type="reset" variant="outline">Reset</ui-button>
    </div>
  </form>
  <output><slot name="#result.text"></slot></output>
</custom-element>

Open a popover

popovertarget and popovertargetaction work as on a native button. The target is looked up by id in the same document or shadow root.

Previewindex.html
Toggle popover
Opened from a ui-button.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button variant="outline" popovertarget="button-popover">Toggle popover</ui-button>
  <div id="button-popover" popover>Opened from a ui-button.</div>
</body>
</html>

Invoke a command

commandfor and command are the native invoker commands. Browsers without native support get the same behaviour. The button dispatches a cancelable command event on the target and then calls showModal(), close(), requestClose(), togglePopover(), showPopover() or hidePopover(). When the target is a component such as ui-dialog rather than a native <dialog>, show-modal, close and request-close are sent as --show-modal, --close and --request-close, because browsers only deliver built-in dialog commands to native dialogs.

Previewindex.html
Open dialog

This dialog was opened with commandfor.

Close
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Button demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <ui-button commandfor="button-dialog" command="show-modal">Open dialog</ui-button>
  <dialog id="button-dialog">
    <p>This dialog was opened with commandfor.</p>
    <ui-button variant="outline" commandfor="button-dialog" command="close">Close</ui-button>
  </dialog>
</body>
</html>

API

Attributes

Attribute Type Default Description
variant "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" "default" Visual style.
size "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" "default" Height and padding. The icon sizes make a square button.
type "button" | "submit" | "reset" "button" What the button does in its form.
disabled boolean false Disables the button. Reflects to the attribute. Also true while the button is inside a disabled <fieldset>.
aria-label string Accessible name, passed to the inner button. Use it for icon-only buttons. Property name: label.
aria-description string Accessible description, passed to the inner button. ui-tooltip sets it from its content. Property name: description.
popovertarget string Id of a popover element to toggle.
popovertargetaction "toggle" | "show" | "hide" Action taken on the popover target.
commandfor string Id of the element that receives command.
command string Command to invoke, such as show-modal, close, request-close, toggle-popover, show-popover, hide-popover or a custom --command.

Events

ui-button dispatches no events of its own. Listen for click as on any element. When native invoker commands are not supported, the button dispatches a cancelable command event on the commandfor target with command and source properties. Calling preventDefault() stops the built-in action.

Slots

Slot Description
(default) The label and any icons. Slotted <svg> elements are sized to calc(var(--spacing) * 4).

CSS parts

Part Description
button The inner native <button>.

CSS states and custom properties

Name Description
--ui-radius-start Overrides the start corner radii. ui-button-group sets it to 0 to join buttons.
--ui-radius-end Overrides the end corner radii.
:state(attr-variant-is-*), :state(attr-size-is-*) Set for the current variant and size, for example ui-button:state(attr-variant-is-outline).

Forms

ui-button is form-associated through ElementInternals, so it finds its form even across shadow roots. type="submit" calls requestSubmit() on the form, which runs constraint validation and fires submit. type="reset" calls reset(). The button itself adds no name or value to the submitted data, and the submit event's submitter is null.

Accessibility

The host delegates focus to the inner native <button>, so Enter and Space activate it as usual. aria-label is passed to the inner button. A disabled button is removed from the tab order.

Edit this page on GitHub · View as Markdown