Skip to content

Button

Run an action, open a URL or navigate to a route with a single visual API.

Usage

Changes in this example: 0

meeko
<script lang="ts">
  const count = signal(0)
</script>

<template>
<UIButton on:click="() => count.value++">Save changes</UIButton>
<UIButton variant="outline">Cancel</UIButton>
<UIButton size="sm" disabled>Unavailable</UIButton>
<p role="status">Changes: {count.value}</p>
</template>

Route, link or button

UIButton renders the element that matches what it does, with the same look: with to, a link to a route of the application that navigates without reloading; with href, an <a> to any URL; with neither, a <button type="button">.

Clicks on these three: 0

meeko
<!-- A route of the application: navigates without reloading, with typed params and search -->
<UIButton to="/links">See links</UIButton>
<UIButton to="/users/[id]" bind:params="{ id: user.id }"
  bind:search="{ tab: 'activity' }">Profile</UIButton>

<!-- Any URL: an <a>; with target="_blank" it adds rel="noopener noreferrer" -->
<UIButton href="https://github.com/meekojs/meeko" target="_blank"
  variant="outline">GitHub</UIButton>
<UIButton href="/report.pdf" download="report.pdf">Download</UIButton>

<!-- Without to or href: a <button type="button"> -->
<UIButton type="submit" bind:disabled="saving.value">Save</UIButton>

<!-- id, title, aria-* and data-* reach the element -->
<UIButton aria-describedby="hint" data-testid="reset" on:click="reset">Reset</UIButton>

Combinations that make no sense are type errors in meeko check and in the editor: to with href, type or disabled on a link, target on a button, a route that does not exist or a dynamic route without params. A link cannot be disabled: it has no native state, and aria-disabled without removing href would still navigate. If the action is not available, do not render the link, or use a button with disabled.

Sizes and states

Danger, loading and icon

tone="danger" marks a destructive action. loading keeps the text, adds the spinner, announces aria-busy and blocks the action without taking its focus. With iconOnly, the button is square: always name it with ariaLabel.

meeko
<UIButton tone="danger">Delete</UIButton>
<UIButton tone="danger" variant="outline">Revoke</UIButton>
<UIButton bind:loading="saving.value">Save</UIButton>
<UIButton iconOnly ariaLabel="Delete" variant="outline">
  <UIIcon name="lucide:trash-2" />
</UIButton>
Props
Name Type / default Description
to RouteId A route of the application. Renders a link that navigates without reloading. Excludes href, type and disabled.
params / search route params / object With to: the params of a dynamic route (required there) and the query string.
href string Any URL. Renders an <a>. Excludes to, type and disabled.
target / rel / download string With to or href. For _blank, noopener noreferrer is added when rel is not set.
type button | submit | reset button Without to or href. Native behavior in forms.
disabled boolean false Without to or href. Disables the native button.
variant primary | secondary | outline | ghost primary Visual hierarchy of the action.
size sm | md | lg md Size of the control.
shape rounded | pill rounded Shape of the outline.
tone danger Marks a destructive action, with any variant.
loading boolean false Adds the spinner, announces aria-busy and blocks the action without taking its focus.
iconOnly / ariaLabel boolean / string false A square button with an icon; ariaLabel gives its accessible name.
name / value / form string Without to or href. The native attributes of a submit button.
class string Additional classes.
id / title / aria-* / data-* HTML attributes Reach the element as they are.

Events and content

on:click receives the mouse event in all three forms; on a link, event.preventDefault() cancels the navigation. The default snippet is required. For an asynchronous action, bind loading to your signal.

For a link with its own visual hierarchy and the defaults of ui.defaults.link, use UILink.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters