Button

The .btn base class, its ten colour variants, five sizes and three style variants.

.btn is the base class. It works on <button>, <a> and <input type="submit">, and reads --btn-* from @uncinq/component-tokens.

<button class="btn">Send</button>
<a class="btn btn-secondary" href="/contact">Contact</a>

Variants compose freely: class="btn btn-danger btn-sm btn-ghost".

Colour variants

ClassIntent
.btn-primaryPrimary action
.btn-secondarySecondary action
.btn-brandBrand-coloured
.btn-neutralNeutral emphasis
.btn-darkDark surface
.btn-lightLight surface
.btn-successConfirmation
.btn-dangerDestructive action
.btn-warningWarning
.btn-infoInformational

Each sets four properties together: background, border, their hover values, and the text colour.

.btn-danger {
  --btn-color-background: var(--color-danger);
  --btn-color-background-hover: var(--color-danger-strong);
  --btn-color-border: var(--color-danger);
  --btn-color-border-hover: var(--color-danger-strong);
  --btn-color-text: var(--color-text-on-danger);
}

The text colour always comes from the matching --color-text-on-* semantic token, so contrast survives a change to the underlying colour. That pairing is the reason to rebrand by redefining --color-brand rather than --btn-color-background.

.btn-primary and .btn-brand resolve to exactly the same values, as do .btn-secondary and .btn-neutral. The intent names exist so a page can say primary action without deciding which colour that is.

Sizes

ClassUse
.btn-xsDense UI, inline controls
.btn-smCompact
.btn-mdDefault, can be omitted
.btn-lgProminent
.btn-xlHero call to action

A size class sets --btn-font-size along with the block and inline padding, all three from the same rung of the scale. Since .btn publishes --icon-size: var(--btn-font-size), any icon inside scales with it.

Style variants

ClassEffect
.btn-ghostFully transparent, background and border alike, text in --color-text
.btn-linkRenders as an underlined link
.btn-controlForm control styling, for buttons sitting next to inputs

.btn-control maps the --btn-color-* set onto the --btn-control-* tokens, which is the same palette the close and menu buttons use by default. It is what makes a button read as part of a form rather than as a call to action.

Every button already carries one. The base class declares text-decoration-line: var(--btn-text-decoration-line), which the tokens set to underline, and paints it with --btn-color-text-decoration, which defaults to transparent.

.btn-link only changes the colour:

.btn-link {
  --btn-color-text-decoration: initial;   /* revealed, takes currentColor */

  @media (hover: hover) and (pointer: fine) {
    &:hover {
      --btn-color-text-decoration: transparent;  /* hidden again */
    }
  }
}

The underline is therefore always in the layout and never shifts the text, which is why hovering one does not nudge the label. If you write your own link-like variant, change the decoration colour rather than the decoration line, for the same reason.

States

:focus-visible draws the outline from the --focus-* tokens. :disabled and [aria-disabled='true'] are treated alike, both getting cursor: not-allowed and --opacity-disabled.

Hover is guarded by @media (hover: hover) and (pointer: fine), so a touch device never leaves a button stuck in its hover colours after a tap. The transition is guarded by prefers-reduced-motion.