Skip to content

Button

Variants

html
<button class="button">Text</button>
<button class="button outlined">Outlined</button>
<button class="button tonal">Tonal</button>
<button class="button filled">Filled</button>
<button class="button elevated">Elevated</button>

Text

Link
html
<button class="button">Text</button>
<button class="button" disabled>Disabled</button>
<a href="#" class="button">Link</a>

Outlined

Link
html
<button class="button outlined">Outlined</button>
<button class="button outlined" disabled>Disabled</button>
<a href="#" class="button outlined">Link</a>

Tonal

Link
html
<button class="button tonal">Tonal</button>
<button class="button tonal" disabled>Disabled</button>
<a href="#" class="button tonal">Link</a>

Filled

Link
html
<button class="button filled">Filled</button>
<button class="button filled" disabled>Disabled</button>
<a href="#" class="button filled">Link</a>

Elevated

Link
html
<button class="button elevated">Elevated</button>
<button class="button elevated" disabled>Disabled</button>
<a href="#" class="button elevated">Link</a>

Buttons with icon and label

html
<!-- Label + icon -->
<button class="button">
  Label
  <svg></svg>
</button>

<!-- Icon + label -->
<button class="button">
  <svg></svg>
  Label
</button>

Icon button

The .sr-only (screen reader only) class removes the visible text but still allows screen readers to access it.

html
<button class="button">
  <span class="sr-only">Label</span>
  <svg></svg>
</button>

Without label

If you can't provide a label or want to control the button from its root you can use the .icon-only class.

html
<button class="button icon-only">
  <svg></svg>
</button>

File upload

Sizes

Resize any button with the .small and .large modifiers.

html
<button class="button small">Small</button>
<button class="button">Medium</button>
<button class="button large">Large</button>

Disabled

Add disabled styling with the disabled attribute or the .disabled class.

html
<button class="button" disabled>Label</button>

<button class="button" disabled>
  <span class="sr-only">Label</span>
  <svg></svg>
</button>

Ripple effect

The ripple effect on button press is enabled by default. Here's how you disable it.

Either disable it by setting the ripple size to 0 in your theme config:

css

--button-ripple-size: 0;

... or to your button-variants.css file and remove all the ripple related styles:

css
:where(button, .button) {
  /* ... */

  /* Ripple effect */
  background-position: center;
  transition: background 0.8s;

  &:where(:not(.disabled, [disabled])) {
    &:where(:not(:active):hover) {
      --ripple: radial-gradient(circle, transparent 1%, var(--_bg-color) 1%) center/15000%;
    }

    &:where(:hover:active) {
      background-size: 100%;
      transition: background 0s;
    }
  }
}

Colors

These are the out-of-the-box colors generated by the Open Props UI theme.css file. You are free to add as many and as few as you want to your styles.

Anatomy

  1. Container
  2. Label text (optional)
  3. Icon (optional)

API

These are the classes and attributes a button can be styled with. As usual, feel free to add your own!

TypeModifiersDefaultDescription
Disabled[disabled], .disabled-If applied, the button is disabled.
Icon-only.icon-only-If applied, the button won't show its inner label.
Sizes.small, .medium, .large.mediumThe size of the button.
Variants.text, .outlined, .tonal, .filled, .elevated.textThe variant to use.

Browser compatibility

Installation

css
:where(.button, input:is([type="button"], [type="submit"], [type="reset"])),
:where(input[type="file"])::-webkit-file-upload-button,
:where(input[type="file"])::file-selector-button {
  --_bg-color: initial;
  --_border-color: initial;
  --_border-radius: var(--button-border-radius, initial);
  --_font-size: initial;
  --_text-color: initial;

  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  align-items: center;
  background: var(--_bg-color) var(--ripple, none);
  border-radius: var(--_border-radius);
  border: var(--border-size-1) solid var(--_border-color);
  color: var(--_text-color);
  display: inline-flex;
  font-size: var(--_font-size);
  font-weight: 700;
  gap: var(--size-2);
  justify-content: center;
  padding-block: 0.5ex;
  padding-inline: 1.5ex;
  text-align: center;
  text-decoration: none;
  transition:
    background-color 0.2s var(--ease-out-3),
    box-shadow 0.2s var(--ease-out-3),
    border-color 0.2s var(--ease-out-3),
    color 0.2s var(--ease-out-3);
  user-select: none;
}

/* file input */
:where(input[type="file"]) {
  align-self: flex-start;
  border-radius: var(--radius-2);
  border: var(--border-size-1) solid var(--surface-filled);
  box-shadow: var(--inner-shadow-4);
  color: var(--text-color-2-contrast);
  cursor: initial;
  max-inline-size: 100%;
  padding: 0;
}

:where(input[type="file"])::-webkit-file-upload-button,
:where(input[type="file"])::file-selector-button {
  cursor: pointer;
  margin-inline-end: var(--size-relative-6);
}
css
:where(.button) {
  --_bg-color: transparent;
  --_border-color: transparent;
  --_text-color: var(--primary);

  /* TODO */
  &:where(:not(.disabled, [disabled])) {
    &:where(:not(:active):hover) {
      --_bg-color: light-dark(
        color-mix(in oklch, white 90%, black),
        color-mix(in oklch, white 40%, black)
      );
    }

    &:where(:hover:active) {
      --_bg-color: light-dark(
        color-mix(in oklch, white 95%, black),
        color-mix(in oklch, white 45%, black)
      );
    }
  }

  &:where(.disabled, [disabled]) {
    opacity: 0.64;
    --_text-color: color-mix(
      in oklch,
      var(--text-color-2) 50%,
      var(--surface-default)
    );
    cursor: not-allowed;
  }

  /* Sizes */
  &.small {
    padding-block: 0;
    padding-inline: 1ex;
  }

  &.medium {
    padding-block: 0.5ex;
    padding-inline: 1.5ex;
  }

  &.large {
    padding-block: 1ex;
    padding-inline: 2.5ex;
  }

  &.text {
    --_bg-color: transparent;
    --_border-color: transparent;
    --_text-color: initial;
  }

  /* Variants */
  &.outlined {
    --_bg-color: var(--surface-default);
    --_border-color: var(--color-8);
    --_text-color: var(--color-8);

    &:where(:not(.disabled, [disabled])) {
      &:where(:not(:active):hover) {
        --_bg-color: var(--color-10);
        --_border-color: var(--color-10);
        --_text-color: var(--color-1);
      }

      &:where(:active) {
        --_bg-color: var(--color-9);
        --_border-color: var(--color-9);
        --_text-color: var(--color-1);
      }
    }

    &:where(.disabled, [disabled]) {
      --_bg-color: var(--surface-default);
      --_border-color: color-mix(
        in oklch,
        var(--text-color-2) 20%,
        var(--surface-default)
      );
      --_text-color: color-mix(
        in oklch,
        var(--text-color-2) 40%,
        var(--surface-default)
      );
    }
  }
  &.tonal {
    --_bg-color: var(--color-5);
    --_text-color: var(--color-16);

    &:where(:not(.disabled, [disabled])) {
      &:where(:not(:active):hover) {
        --_bg-color: var(--color-9);
        --_border-color: var(--color-9);
      }

      &:where(:active) {
        --_bg-color: var(--color-7);
        --_border-color: var(--color-7);
      }
    }

    &:where(.disabled, [disabled]) {
      --_bg-color: color-mix(
        in oklch,
        var(--text-color-2) 8%,
        var(--surface-default)
      );
      --_text-color: color-mix(
        in oklch,
        var(--text-color-2) 70%,
        var(--surface-default)
      );
    }
  }
  &.filled {
    --_bg-color: var(--color-8);
    --_text-color: var(--color-1);

    &:where(:not(.disabled, [disabled])) {
      &:where(:not(:active):hover) {
        --_bg-color: var(--color-10);
        --_border-color: var(--color-10);
      }

      &:where(:active) {
        --_bg-color: var(--color-9);
        --_border-color: var(--color-9);
      }
    }

    &:where(.disabled, [disabled]) {
      --_bg-color: color-mix(
        in oklch,
        var(--text-color-2) 20%,
        var(--surface-default)
      );
      --_text-color: color-mix(
        in oklch,
        var(--text-color-2) 70%,
        var(--surface-default)
      );
    }
  }
  &.elevated {
    --_bg-color: light-dark(var(--gray-1), var(--gray-12));
    --_text-color: var(--color-8);

    box-shadow: var(--shadow-3);

    &:where(:not(.disabled, [disabled])) {
      &:where(:not(:active):hover) {
        box-shadow: var(--shadow-4);
      }

      &:where(:active) {
        box-shadow: var(--shadow-6);
      }
    }

    @container style(--color-scheme: dark) {
      box-shadow: var(--shadow-4);

      &:where(:not(.disabled, [disabled])) {
        &:where(:not(:active):hover) {
          box-shadow: var(--shadow-5);
        }

        &:where(:active) {
          box-shadow: var(--shadow-6);
        }
      }
    }

    &:where(.disabled, [disabled]) {
      --_bg-color: color-mix(
        in oklch,
        var(--text-color-2) 8%,
        var(--surface-elevated)
      );
      --_text-color: color-mix(
        in oklch,
        var(--text-color-2) 70%,
        var(--surface-elevated)
      );
    }
  }

  /* Ripple effect */
  background-position: center;
  transition: background 0.8s;

  &:where(:not(.disabled, [disabled])) {
    &:where(:not(:active):hover) {
      --ripple: radial-gradient(circle, transparent 1%, var(--_bg-color) 1%)
        center/15000%;
    }

    &:where(:hover:active) {
      background-size: var(--button-ripple-size);
      transition: background 0s;
    }
  }
}
css
/*
Inspired by ModernCSS.dev
https://moderncss.dev/modern-css-for-dynamic-component-based-architecture/#creating-variant-styles-with-has

The `.sr-only` class refers to the "screen reader only" util which
removes the visible text but still allows screen readers to
access it.
*/
:where(.button) {
  /* Base */
  &:where(:has(svg), &.icon-only) {
    gap: 1ex;

    & svg {
      fill: currentColor;
    }
  }

  /* Icon-only */
  &:where(:has(.sr-only), &.icon-only) {
    --_text-color: inherit;

    aspect-ratio: 1;
    border: 0;
    border-radius: var(--button-border-radius);
    gap: 0;
    padding: 0.5ex;

    svg {
      inline-size: 85%;
      pointer-events: none;
    }

    &.small {
      padding: 0.125ex;
      & * {
        scale: 0.9;
        transform-origin: center;
      }
    }

    &.large {
      padding: 1ex;
      & * {
        scale: 1.1;
        transform-origin: center;
      }
    }
  }
}