oriUI

Button

A styled, accessible button. Dynamic state is expressed through real attributes — disabled becomes a true disabled (or aria-disabled for link buttons), loading sets aria-busy, and it ships a visible :focus-visible ring.

The examples are organized by layer: the class reference is the standalone @oriui/css layer, and the Framework API is the @oriui/vue component. Every example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte / plain-HTML usage), Vue; HTML is the default.

Classes

A button is a block class plus single-class token utilities — one class repoints one token, no base class needed. The Vue props in Framework API map 1:1 to these.

ClassTypeDescription
ori-buttonBlockRequired base class.
ori-variant_*Stylesolid · soft · outline · text · quiet
ori-color_*Colorprimary · secondary · success · warning · danger · info · surface
ori-button_* (size)Sizeinherit · xs · sm · md · lg · xl · xxl
ori-font-size_*Fontxs · sm · md · lg · xl · xxl (scales the label)
ori-size-radius_*Radiusnone · xs · sm · md · lg · xl · full
ori-button_fluid · ori-button_iconLayoutfull-width · icon-only
ori-button__icon · ori-button__textParticon / label elements
disabled · aria-busy · data-activeStatereal attributes, not classes

À la carte: the classes above ship in @oriui/css/components/button.css. Import a foundation (@oriui/css/base.css or @oriui/css/tokens.css) first — the token utilities (ori-color_*, ori-size-radius_*, …) live there, not in the component file. The full bundle @oriui/css is the default and already carries both; see à-la-carte imports.

The non-fill variants (soft / outline / text / quiet) paint the label with the AA-safe --ori-color-text tone rather than the raw role; solid keeps --ori-color-on for its solid background — see Design tokens.

Variants

Five visual styles, all driven by the ori-variant_* single-class token.

html
<button class="ori-button ori-variant_solid ori-color_primary">Solid</button>
<!-- swap the variant: ori-variant_solid → _soft / _outline / _text / _quiet -->
<button class="ori-button ori-variant_outline ori-color_primary">Outline</button>
vue
<OriButton label="Solid" variant="solid" />
<OriButton label="Soft" variant="soft" />
<OriButton label="Outline" variant="outline" />
<OriButton label="Text" variant="text" />
<OriButton label="Quiet" variant="quiet" />

Colors

Every semantic role. surface / background are also available for neutral buttons.

html
<button class="ori-button ori-variant_solid ori-color_danger">danger</button>
vue
<OriButton label="primary" color="primary" />
<OriButton label="danger" color="danger" />
<OriButton label="info" color="info" />

Variant × color compose freely — e.g. a tonal danger button:

html
<button class="ori-button ori-variant_soft ori-color_danger">Delete</button>
vue
<OriButton label="Delete" variant="soft" color="danger" />
<OriButton label="Confirm" variant="outline" color="success" />

Sizes

xs → xxl. The size sugar ori-button_* drives the height; ori-font-size_* scales the label.

html
<button class="ori-button ori-button_sm ori-font-size_sm">sm</button>
<button class="ori-button ori-button_xl ori-font-size_xl">xl</button>
vue
<OriButton label="sm" size="sm" />
<OriButton label="xl" size="xl" />

Radius

From none to the default full (pill).

html
<button class="ori-button ori-size-radius_none">none</button>
vue
<OriButton label="none" radius="none" />
<OriButton label="full" radius="full" />

With icon

Pass an SVG path to icon. iconPosition places it: start (the default), end, top or bottom — start and end are logical, so they swap sides in RTL. For an icon-only square, pass icon withoutlabel (give it an aria-label) — icon mode is triggered by the explicit icon prop, so a slot-only or text button is never forced into a square.

html
<button class="ori-button ori-button_icon ori-variant_soft" aria-label="Add">
    <i class="ori-icon" aria-hidden="true"
        ><svg viewBox="0 0 24 24"><path d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" /></svg
    ></i>
</button>
vue
<OriButton label="Add" icon="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" />
<OriButton label="Next" icon="M12 4l-1.41…" iconPosition="end" />
<!-- icon-only square: pass `icon` with no `label`, add an accessible name -->
<OriButton icon="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" aria-label="Add" variant="soft" />

Loading

loading swaps the icon for a spinner, sets aria-busy="true", and blocks interaction.

html
<button class="ori-button" aria-busy="true">
    <span class="ori-spinner ori-spinner_inline" role="status" aria-hidden="true"></span>
    <span class="ori-button__text">Saving</span>
</button>
vue
<OriButton label="Saving" loading />
<OriButton icon="…" aria-label="Saving" loading />

States

active and pressed are different things, and the difference is the whole toggle contract.

active paints a forced :active look (data-active) and announces nothing — use it for a transient highlight. pressed is the toggle state: it renders aria-pressed, so assistive tech reports the button as on or off, and it paints the pressed affordance (a tint plus an inset ring) on the button itself, with no toolbar ancestor required. Omit pressed entirely on a plain action button — binding it to false is a claim that the control is a toggle that happens to be off.

disabled is the real attribute.

html
<button class="ori-button" data-active>Active</button> <button class="ori-button" disabled>Disabled</button>
vue
<OriButton label="Active" active />
<OriButton label="Disabled" disabled />

Fluid

fluid stretches the button to the full width of its container.

html
<button class="ori-button ori-button_fluid">Full width</button>
vue
<OriButton label="Full width" fluid />

Common patterns

A confirm / cancel pair and an icon toolbar — the everyday compositions.

html
<div style="display: flex; gap: 0.5rem">
    <button class="ori-button ori-variant_text">Cancel</button>
    <button class="ori-button ori-variant_solid">Save changes</button>
</div>
vue
<div style="display: flex; gap: 0.5rem">
    <OriButton label="Cancel" variant="text" />
    <OriButton label="Save changes" variant="solid" />
</div>

Accessibility

The accessibility contract holds across every layer — the standalone classes and the Vue component render the same attributes and keyboard behavior.

  • Renders a real <button type="button"> by default; as="a" (or a router link) switches the tag and uses aria-disabled + tabindex="-1" instead of the boolean disabled.
  • loading sets aria-busy="true"; the spinner is aria-hidden. An icon-only button needs an aria-label.
  • Visible :focus-visible outline; state lives in attributes, not classes.
KeyAction
EnterActivates the button.
SpaceActivates the button (native only).

Framework API

The props, events, slots, and polymorphism of the Vue component. The standalone CSS layer has no component API — its surface is the classes above.

Props

PropTypeDefaultDescription
labelstring—Visible button text. A labeled (or slotted) button is a normal button, never an icon square.
variant'solid' | 'soft' | 'outline' | 'text' | 'quiet''solid'Visual style.
colorThemeColor'primary'Semantic role: primary · secondary · success · warning · danger · info · surface.
sizeActionSize'md'Height + label scale (xs–xxl).
radiusRadiusSize'full'Corner radius (none–full).
iconstring—SVG path for an icon; hidden while loading. icon with no label → an icon-only square (ori-button_icon).
iconPosition'top' | 'bottom' | 'start' | 'end''start'Icon placement around the label.
loadingbooleanfalseShows a spinner, sets aria-busy, and blocks interaction.
disabledbooleanfalseReal disabled (button) or aria-disabled + tabindex="-1" (other tags).
activebooleanfalseForced :active LOOK via data-active. Not a toggle state — see pressed.
pressedboolean—Toggle STATE: renders aria-pressed and the pressed affordance. Omit it on a plain action button.
fluidbooleanfalseFull-width (block) button.
asstring | Component'button'Element or component to render (e.g. 'a', a router link).

Events & attributes

OriButton declares no custom events. It doesn't set inheritAttrs: false, so native listeners (@click, @focus, …) and attributes (type, aria-label, name, form, …) fall through to the rendered element — the tag given by as.

Slots

SlotDescription
defaultReplaces the built-in content (icon + text). Supply your own markup; you own its layout/spacing.

Polymorphic (as)

Render any tag or component. As a non-<button>, disabled becomes aria-disabled + tabindex="-1" instead of the boolean attribute.

html
<a href="/docs" class="ori-button ori-variant_outline">Link button</a>
vue
<!-- a real link… -->
<OriButton as="a" href="/docs" label="Link button" variant="outline" />
<!-- …or a router link -->
<OriButton :as="RouterLink" to="/docs" label="Go to docs" />