Skip to content

cv-button

Interactive element that triggers an action or toggles a pressed state.

Headless: createButton

Usage

View source
html
<div class="button-demo-shell" data-demo="button" data-live-demo-height="780">
  <section class="button-demo-hero" aria-labelledby="button-demo-title">
    <div class="button-demo-copy">
      <span class="button-demo-kicker">Action primitive</span>
      <h3 id="button-demo-title">
        Use button when a user commits intent, changes mode, or starts a safe action.
      </h3>
      <p>
        The component keeps activation, toggle, disabled, loading, and form behavior in the headless button
        model. UIKit owns only the action surface, slots, variants, and sizing.
      </p>
    </div>

    <dl class="button-demo-metrics" aria-label="Button contract summary">
      <div>
        <dt>Variants</dt>
        <dd>default / primary / danger / ghost</dd>
      </div>
      <div>
        <dt>States</dt>
        <dd>disabled / loading / pressed</dd>
      </div>
      <div>
        <dt>Slots</dt>
        <dd>prefix + suffix</dd>
      </div>
    </dl>
  </section>

  <section class="button-demo-board" aria-label="Button examples in a vault workflow">
    <div class="button-demo-command">
      <div class="button-demo-command-head">
        <div>
          <span>Visible vault route</span>
          <strong>travel-profile.visible</strong>
        </div>
        <span class="button-demo-status">safe action window</span>
      </div>

      <div class="button-demo-layer">
        <span>Operator decision</span>
        <p>Commit the visible profile, inspect routing proof, or block relay writes before export.</p>
      </div>

      <div class="button-demo-actions" aria-label="Primary action examples">
        <cv-button preset="action-primary">
          <svg class="button-demo-icon" slot="prefix" viewBox="0 0 24 24" aria-hidden="true">
            <path d="M20 6 9 17l-5-5"></path>
          </svg>
          Commit visible profile
        </cv-button>

        <cv-button outline>
          Inspect route
          <svg class="button-demo-icon" slot="suffix" viewBox="0 0 24 24" aria-hidden="true">
            <path d="m9 18 6-6-6-6"></path>
          </svg>
        </cv-button>

        <cv-button variant="ghost" outline>
          <svg class="button-demo-icon" slot="prefix" viewBox="0 0 24 24" aria-hidden="true">
            <path d="M12 5v14"></path>
            <path d="M5 12h14"></path>
          </svg>
          Export proof
        </cv-button>

        <cv-button variant="danger" outline>
          <svg class="button-demo-icon" slot="prefix" viewBox="0 0 24 24" aria-hidden="true">
            <path d="M18 6 6 18"></path>
            <path d="m6 6 12 12"></path>
          </svg>
          Disable relay
        </cv-button>
      </div>
    </div>

    <aside class="button-demo-side" aria-label="Button state examples">
      <div class="button-demo-side-head">
        <span class="button-demo-kicker">State map</span>
        <h4>Each state changes affordance without changing the contract.</h4>
      </div>

      <div class="button-demo-state-list">
        <div>
          <span>Loading blocks action</span>
          <cv-button variant="primary" loading>Committing</cv-button>
        </div>
        <div>
          <span>Disabled stays visible</span>
          <cv-button disabled>Unavailable</cv-button>
        </div>
        <div>
          <span>Toggle reflects pressed</span>
          <cv-button toggle pressed>Policy pinned</cv-button>
        </div>
      </div>
    </aside>
  </section>

  <section class="button-demo-section" aria-labelledby="button-demo-matrix-title">
    <div class="button-demo-section-header">
      <span class="button-demo-kicker">Variants, sizes, and shape</span>
      <h4 id="button-demo-matrix-title">
        Use one action hierarchy, then tune emphasis with variant and modifier.
      </h4>
    </div>

    <div class="button-demo-matrix" aria-label="Button variant and size matrix">
      <div>
        <span>Default</span>
        <cv-button>Review policy</cv-button>
        <cv-button outline>Quiet secondary</cv-button>
      </div>
      <div>
        <span>Primary</span>
        <cv-button variant="primary">Save trust route</cv-button>
        <cv-button variant="primary" outline>Primary outline</cv-button>
      </div>
      <div>
        <span>Danger</span>
        <cv-button variant="danger">Block export</cv-button>
        <cv-button variant="danger" outline>Danger outline</cv-button>
      </div>
      <div>
        <span>Ghost</span>
        <cv-button variant="ghost">Open details</cv-button>
        <cv-button variant="ghost" outline>Ghost outline</cv-button>
      </div>
      <div>
        <span>Size</span>
        <cv-button size="small">Small</cv-button>
        <cv-button>Medium</cv-button>
        <cv-button size="large">Large</cv-button>
      </div>
      <div>
        <span>Shape and preset</span>
        <cv-button pill>Rounded action</cv-button>
        <cv-button preset="action-primary-subtle">Subtle primary</cv-button>
      </div>
    </div>
  </section>
</div>

Anatomy

<cv-button> (host)
└── <button part="base" type="button" role="button">
    ├── <span part="spinner" aria-hidden="true">   ← only when [loading]
    ├── <span part="prefix">
    │   └── <slot name="prefix">
    ├── <span part="label">
    │   └── <slot>
    └── <span part="suffix">
        └── <slot name="suffix">

Attributes

AttributeTypeDefaultDescription
disabledBooleanfalsePrevents interaction
toggleBooleanfalseEnables toggle (pressed) behavior
pressedBooleanfalsePressed state (only meaningful when toggle is true)
loadingBooleanfalseShows spinner and blocks interaction
variantString"default"Visual variant: default | primary | danger | ghost
presetStringSemantic preset: action-primary | action-primary-subtle
outlineBooleanfalseOutlined appearance (transparent background, visible border)
pillBooleanfalseFully rounded edges (border-radius: 999px)
sizeString"medium"Size: small | medium | large
typeString"button"Form action type: button | submit | reset

Form Behavior

  • type="button": no form action.
  • type="submit": triggers form submission for nearest form owner.
  • type="reset": triggers form reset for nearest form owner.
  • Form owner resolution order:
    1. host [form="..."] attribute by element id
    2. nearest ancestor form via closest('form')
  • If disabled or loading is active, submit/reset actions are blocked.

Variants

VariantDescription
defaultDefault filled style with surface background and border
primaryPrimary-tinted background and border using --cv-color-primary
dangerDanger-tinted background and border using --cv-color-danger
ghostTransparent background and border

The outline boolean modifier can be combined with any variant to produce an outlined appearance (transparent background, visible border tinted by variant color).

Presets

PresetDescription
action-primaryStrong primary action surface for form/footer actions
action-primary-subtleSofter primary action surface for inline edit actions

Sizes

Size--cv-button-min-height--cv-button-padding-inline--cv-button-padding-block--cv-button-font-size
small30pxvar(--cv-space-2, 8px)var(--cv-space-1, 4px)var(--cv-font-size-sm, 13px)
medium36pxvar(--cv-space-3, 12px)var(--cv-space-2, 8px)var(--cv-font-size-base, 14px)
large42pxvar(--cv-space-4, 16px)var(--cv-space-2, 8px)var(--cv-font-size-md, 16px)

Slots

SlotDescription
(default)Button label
prefixIcon or element before label
suffixIcon or element after label

CSS Parts

PartElementDescription
base<button>Root interactive element with role="button"
label<span>Wrapper around the default slot
prefix<span>Wrapper around the prefix slot
suffix<span>Wrapper around the suffix slot
spinner<span>Loading spinner (rendered only when loading is true)

CSS Custom Properties

PropertyDefaultDescription
--cv-button-min-height36pxMinimum block size of the button
--cv-button-padding-inlinevar(--cv-space-3, 12px)Horizontal padding
--cv-button-padding-blockvar(--cv-space-2, 8px)Vertical padding
--cv-button-border-radiusvar(--cv-radius-sm, 6px)Border radius for button shape
--cv-button-gapvar(--cv-space-2, 8px)Spacing between button content parts
--cv-button-font-sizevar(--cv-font-size-base, 14px)Font size of button content

Additionally, component styles depend on theme tokens through fallback values:

Theme PropertyDefaultDescription
--cv-color-border#2a3245Base border color
--cv-color-surface#141923Surface background color
--cv-color-text#e8ecf6Default text color
--cv-color-primary#65d7ffPrimary accent color
--cv-color-danger#ff7d86Danger accent color
--cv-duration-fast120msTransition duration
--cv-easing-standardeaseTransition timing function
--cv-radius-sm6pxBase radius used for button fallback
--cv-space-14pxSmall spacing scale fallback
--cv-space-28pxMedium spacing scale fallback
--cv-space-312pxMedium-large spacing scale fallback
--cv-space-416pxLarge spacing scale fallback

Visual States

Host selectorDescription
:host([disabled])Reduced opacity (0.55), cursor: not-allowed
:host([pressed])Primary-tinted background (per variant)
:host([loading])Shows spinner, label reduced opacity (0.72), cursor: progress
:host([variant="default"])Default surface background with border
:host([variant="primary"])Primary-tinted background and border
:host([variant="danger"])Danger-tinted background and border
:host([variant="ghost"])Transparent background and border
:host([outline])Transparent background, visible border (tinted by variant)
:host([pill])Fully rounded edges
:host([size="small"])Small size overrides
:host([size="large"])Large size overrides

Reactive State Mapping

cv-button is a visual adapter over headless createButton.

UIKit PropertyDirectionHeadless Binding
disabledattr → actionactions.setDisabled(value)
loadingattr → actionactions.setLoading(value)
pressedattr → actionactions.setPressed(value)
toggleattr → optionpassed as isPressed presence in createButton(options)
Headless StateDirectionDOM Reflection
state.isDisabled()state → attr[disabled] host attribute
state.isLoading()state → attr[loading] host attribute
state.isPressed()state → attr[pressed] host attribute
  • contracts.getButtonProps() is spread onto the inner [part="base"] element to apply role, aria-disabled, aria-busy, aria-pressed, tabindex, and keyboard/click handlers.
  • UIKit dispatches cv-input and cv-change events by observing isPressed changes triggered by user activation (not by controlled setPressed).
  • UIKit does not own activation or toggle logic; headless state is the source of truth.

Events

EventDetailDescription
cv-input{pressed: boolean, toggle: boolean}Fires on activation in toggle mode only
cv-change{pressed: boolean}Fires when pressed state changes (toggle mode only)

Normal (non-toggle) buttons rely on the native click event.

Legacy Button Migration Mapping

Variant mapping

  • brand -> primary
  • neutral -> default
  • secondary -> default
  • plain -> ghost
  • text -> ghost
  • primary -> primary
  • danger -> danger
  • ghost -> ghost

Slot mapping

  • slot="start" -> slot="prefix"
  • slot="end" -> slot="suffix"

ChromVoid UIKit documentation