Skip to content

Button ​

A button fires an action. It carries the press, hover and focus behaviour the rest of the library reuses, which is why a dropdown, a modal and a tooltip each wrap one to make a trigger.

ts
import { Button } from "ropav";
variant
size
template
<Button>Get started</Button>

Variants ​

button-variants.vue
vue
<script setup lang="ts">
import { Button } from "ropav";

const variants = [
  "primary",
  "secondary",
  "tertiary",
  "outline",
  "ghost",
  "danger",
  "danger-soft",
] as const;
</script>

<template>
  <Button v-for="variant in variants" :key="variant" :variant="variant">
    {{ variant }}
  </Button>
</template>

Sizes ​

The default size lives in the base class, so a button with no size never looks unstyled.

button-sizes.vue
vue
<script setup lang="ts">
import { Button } from "ropav";

const sizes = ["xs", "sm", "md", "lg"] as const;
</script>

<template>
  <Button v-for="size in sizes" :key="size" :size="size">{{ size }}</Button>
</template>

Icons ​

An icon goes in the default slot beside the label. A button whose whole content is one icon takes is-icon-only, and needs an aria-label — there is no text left to name it.

button-icons.vue
vue
<script setup lang="ts">
import { Button, IconPlus, IconSearch } from "ropav";
</script>

<template>
  <Button>
    <IconPlus />
    New item
  </Button>

  <Button variant="secondary">
    Search
    <IconSearch />
  </Button>

  <Button aria-label="Search" is-icon-only variant="ghost">
    <IconSearch />
  </Button>
</template>

Pending and disabled ​

is-pending marks an in-flight action. It renders data-pending and blocks activation while keeping the button focusable, so assistive technology can still reach it and hear what changed. is-disabled is the other choice on purpose: a disabled button leaves the tab order entirely.

A pending button that would otherwise submit a form is switched to type="button" while it waits. Blocking the click is not enough on its own — implicit submission reaches the form through the button's type, without a click ever landing.

button-pending.vue
vue
<script setup lang="ts">
import { Button } from "ropav";
import { ref } from "vue";

const isPending = ref(false);

const save = () => {
  isPending.value = true;
  setTimeout(() => (isPending.value = false), 2000);
};
</script>

<template>
  <Button :is-pending="isPending" @click="save">Save changes</Button>
  <Button is-disabled variant="secondary">Disabled</Button>
</template>

Full width ​

button-full-width.vue
vue
<script setup lang="ts">
import { Button } from "ropav";
</script>

<template>
  <div class="stack">
    <Button full-width>Continue</Button>
    <Button full-width variant="secondary">Cancel</Button>
  </div>
</template>

<style scoped>
.stack {
  display: flex;
  width: 100%;
  max-width: var(--rp-container-sm);
  flex-direction: column;
  gap: calc(var(--rp-spacing) * 3);
}
</style>

Styling ​

Every component takes a class prop, appended to the classes its recipe already carries. Component rules live in the components layer and utilities in the later utilities one, so a utility passed this way wins on layer order alone — nothing has to be stripped for it to land.

A colour that more than one state paints goes through a custom property instead, so a single state can be retuned without flattening the rest. Those are set with style, which needs no build of any kind.

template
<!-- `w-full` is a class of yours; the layer order is what makes it land -->
<Button class="w-full" variant="secondary">Save</Button>

<Button style="--button-bg-hover: var(--rp-success)">Publish</Button>

Accessibility ​

  • Renders a native <button>, and type defaults to "button" so it never submits by accident.
  • An explicit tabindex is written even though a native button is already tabbable: Safari does not focus one without it. A disabled button gets none, so it is not reachable at all.
  • Moving into and out of the pending state is announced, but only while the button is focused — that is when the change is part of what the reader is doing.
  • Under Forced Colors Mode the focus ring is redrawn as an outline, because the mode strips box-shadow and every ring in this library is one.

API ​

PropTypeDefaultDescription
fullWidthboolean—Stretches the button to the full width of its container.
isDisabledboolean—Disables the button.
isIconOnlyboolean—Renders the icon-only shape, for a button whose content is a single icon.
isPendingboolean—Marks an in-flight action. Renders data-pending and blocks activation, while keeping the button focusable so assistive technology can still reach it.
size"xs" | "sm" | "md" | "lg""md"Button size.
type"button" | "submit" | "reset""button"Native button type.
variant"primary" | "secondary" | "tertiary" | "outline" | "ghost" | "danger" | "danger-soft""primary"Button variant.

The default slot receives:

Slot propType
isDisabledboolean
isFocusVisibleboolean
isHoveredboolean
isPendingboolean
isPressedboolean
EventPayload
clickMouseEvent