Skip to content

Components

Button

Variants

html
<button class="button">Default</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-only

See Icon button documentation.

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="icon-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.css file and remove all the ripple related styles:

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

  /* Ripple effect */
  background-position: center;

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

      transition: background var(--button-ripple-duration);
    }

    &:where(:hover:active) {
      background-size: var(--button-ripple-size);
      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.

File upload

Is it a button? Is it an input? You can find the docs for it here at least.

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
Sizes.small, default, .large.mediumThe size of the element.
Variantsdefault, .outlined, .tonal, .filled, .elevateddefaultThe variant to use.

Browser compatibility

Installation

css
@layer components.base {
  :where(.button) {
    --_bg-color: transparent;
    --_border-color: transparent;
    --_border-radius: var(--button-border-radius);
    --_font-size: initial;
    --_min-height: 2.375rem;
    --_text-color: var(--primary);

    -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;
    min-block-size: var(--_min-height);
    padding-block: 0.5ex;
    padding-inline: 1.5ex;
    text-align: center;
    text-decoration: none;

    user-select: none;

    &:where([disabled]) {
      cursor: not-allowed;
      opacity: 0.64;
    }

    @media (prefers-reduced-motion: no-preference) {
      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),
        outline-offset 0.05s var(--ease-1);

      /* Ripple effect */
      background-position: center;

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

          transition: background var(--button-ripple-duration);
        }

        &:where(:hover:active) {
          background-size: var(--button-ripple-size);
          transition: background 0s;
        }
      }
    }

    /* Element states */
    &:where(:not([disabled])) {
      &:where(:not(:active):hover) {
        --_bg-color: light-dark(
          oklch(from var(--primary) l 0.01 h / 20%),
          oklch(from var(--primary) l 0.01 h / 40%)
        );
      }

      &:where(:hover:active) {
        --_bg-color: light-dark(
          oklch(from var(--primary) l 0.06 h / 30%),
          oklch(from var(--primary) l 0.06 h / 40%)
        );
      }
    }

    /* Disabled */
    &:where([disabled]) {
      --_text-color: color-mix(
        in oklch,
        var(--text-color-2) 50%,
        var(--surface-default)
      );
    }

    /* Icon */
    &:where(:has(svg), &.icon-only) {
      gap: 1ex;

      svg {
        max-block-size: 0.7lh;
      }

      & svg:not([fill="none"]) {
        fill: currentColor;
      }

      & svg[stroke] {
        stroke: currentColor;
      }
    }

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

    &.large {
      --_min-height: 2.875rem;
      padding-inline: 4ex;
    }

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

      &:where(:not([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]) {
        --_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-6);
      --_text-color: var(--color-16);

      &:where(:not([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]) {
        --_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])) {
        &: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]) {
        --_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(
        color-mix(in oklch, var(--gray-2) 97%, var(--color-16)),
        color-mix(in oklch, var(--gray-13) 97%, var(--color-1))
      );
      --_ripple-color: light-dark(
        color-mix(in oklch, var(--gray-2) 80%, var(--color-8)),
        color-mix(in oklch, var(--gray-13) 80%, var(--color-8))
      );
      --_text-color: var(--color-8);

      box-shadow:
        0px 3px 1px -2px oklch(0 0 0 / 20%),
        0px 2px 2px 0px oklch(0 0 0 / 14%),
        0px 1px 5px 0px oklch(0 0 0 / 12%);

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

          --_bg-color: light-dark(
            color-mix(in oklch, var(--gray-2) 93%, var(--color-8)),
            color-mix(in oklch, var(--gray-13) 93%, var(--color-8))
          );
        }

        &:where(:hover:active) {
          --_bg-color: light-dark(
            color-mix(in oklch, var(--gray-2) 91%, var(--color-8)),
            color-mix(in oklch, var(--gray-13) 91%, var(--color-8))
          );
        }
      }

      &:where([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)
        );
      }
    }
  }

  /* 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);
  }
}