Debase UI

Button

Installation

Add the component to your project and install missing dependencies if needed.

import { css, cx } from "@linaria/core";
import type React from "react";
 
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "danger" | "naked";
  isLoading?: boolean;
}
 
export default function Button({
  variant = "primary",
  children,
  className,
  disabled,
  isLoading,
  ...props
}: Props) {
  const loaderClass = cx(
    loaderStyle,
    variant === "primary" && primaryLoaderStyle,
    variant === "secondary" && secondaryLoaderStyle,
    variant === "danger" && dangerLoaderStyle,
    variant === "naked" && nakedLoaderStyle,
  );
 
  return (
    <button
      className={cx(
        baseButtonStyle,
        loadingStyle,
        variant === "primary" && primaryStyle,
        variant === "secondary" && secondaryStyle,
        variant === "danger" && dangerStyle,
        variant === "naked" && nakedStyle,
        disabled && disabledStyle,
        disabled && variant === "primary" && primaryDisabledStyle,
        disabled && variant === "secondary" && secondaryDisabledStyle,
        disabled && variant === "danger" && dangerDisabledStyle,
        disabled && variant === "naked" && nakedDisabledStyle,
        className,
      )}
      data-loading={isLoading}
      disabled={disabled || isLoading}
      {...props}
    >
      {children}
      {isLoading && (
        <div className={loadingOverlayStyle}>
          <div className={loaderClass} />
        </div>
      )}
    </button>
  );
}
 
const baseButtonStyle = css`
  @layer debase {
    position: relative;
    display: flex;
    height: fit-content;
    align-items: center;
    justify-content: center;
    border: none;
    white-space: nowrap;
    cursor: pointer;
    gap: var(--debase__spacing__x05);
    border-radius: var(--debase__radius);
    font-size: var(--debase__font-size__normal);
    font-family: var(--font-family-default);
    padding: var(--debase__spacing__x1) var(--debase__spacing__x2);
    width: fit-content;
  }
`;
 
const loadingStyle = css`
  @layer debase {
    &[data-loading="true"] {
      user-select: none;
      color: transparent;
    }
  }
`;
 
const loaderStyle = css`
  @layer debase {
    width: 20px;
    aspect-ratio: 1;
    border-radius: 50%;
    animation: l13 1s infinite linear;
    @keyframes l13 { 
      100% { transform: rotate(1turn); }
    }
  }
`;
 
const loadingOverlayStyle = css`
  @layer debase {
    position: absolute;
    top: 0;
    left: 0;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    border-radius: var(--debase__radius);
    display: none;
    [data-loading="true"] & {
      display: flex;
    }
  }
`;
 
const primaryStyle = css`
  @layer debase {
    color: var(--debase__color__primary-label);
    background: linear-gradient(180deg, rgb(255 255 255 / 16%) 0%, rgb(255 255 255 / 0%) 100%), var(--debase__color__primary);
    box-shadow: 0 0 0 1px var(--debase__color__primary), 0 1px 1px 0 rgb(9 9 11 / 15%), 0 0.75px 0 0 rgb(255 255 255 / 50%) inset;
    &:hover:not([data-loading="true"]):not(:disabled) {
      background: var(--hoverEffect), linear-gradient(180deg, rgb(255 255 255 / 16%) 0%, rgb(255 255 255 / 0%) 100%), var(--debase__color__primary);
    }
  }
`;
 
const primaryLoaderStyle = css`
  @layer debase {
    background: radial-gradient(farthest-side,var(--debase__color__primary-label) 94%,#0000) top/4px 4px no-repeat,
                conic-gradient(#0000 30%,var(--debase__color__primary-label));
    mask: radial-gradient(farthest-side, #0000 calc(100% - 4px), #000 0);
  }
`;
 
const secondaryStyle = css`
  @layer debase {
    color: var(--debase__color__text);
    outline: 1px solid var(--debase__color__line);
    background: linear-gradient(180deg, rgb(9 9 11 / 0%) 0%, rgb(9 9 11 / 3%) 100%), var(--debase__color__background);
    &:hover:not([data-loading="true"]):not(:disabled) {
      background: var(--hoverEffect), linear-gradient(180deg, rgb(9 9 11 / 0%) 0%, rgb(9 9 11 / 3%) 100%), var(--debase__color__background);
    }
  }
`;
 
const secondaryLoaderStyle = css`
  @layer debase {
    background: radial-gradient(farthest-side,var(--debase__color__text) 94%,#0000) top/4px 4px no-repeat,
                conic-gradient(#0000 30%,var(--debase__color__text));
    mask: radial-gradient(farthest-side, #0000 calc(100% - 4px), #000 0);
  }
`;
 
const dangerStyle = css`
  @layer debase {
    color: var(--debase__color__danger-label);
    background: linear-gradient(180deg, rgb(255 255 255 / 16%) 0%, rgb(255 255 255 / 0%) 100%), var(--debase__color__danger);
    box-shadow: 0 0 0 1px var(--debase__color__danger), 0 1px 1px 0 rgb(9 9 11 / 15%), 0 0.75px 0 0 rgb(255 255 255 / 50%) inset;
    &:hover:not([data-loading="true"]):not(:disabled) {
      background: var(--hoverEffect), linear-gradient(180deg, rgb(255 255 255 / 16%) 0%, rgb(255 255 255 / 0%) 100%), var(--debase__color__danger);
    }
  }
`;
 
const dangerLoaderStyle = css`
  @layer debase {
    background: radial-gradient(farthest-side,var(--debase__color__danger-label) 94%,#0000) top/4px 4px no-repeat,
                conic-gradient(#0000 30%,var(--debase__color__danger-label));
    mask: radial-gradient(farthest-side, #0000 calc(100% - 4px), #000 0);
  }
`;
 
const nakedStyle = css`
  @layer debase {
    color: var(--debase__color__text);
    background: transparent;
    &:hover:not([data-loading="true"]):not(:disabled) {
      background: var(--hoverEffect);
    }
  }
`;
 
const nakedLoaderStyle = css`
  @layer debase {
    background: radial-gradient(farthest-side,var(--debase__color__text) 94%,#0000) top/4px 4px no-repeat,
                conic-gradient(#0000 30%,var(--debase__color__text));
    mask: radial-gradient(farthest-side, #0000 calc(100% - 4px), #000 0);
  }
`;
 
const disabledStyle = css`
  @layer debase {
    cursor: not-allowed;
  }
`;
 
const primaryDisabledStyle = css`
  @layer debase {
    color: color-mix(in srgb, var(--debase__color__primary-label) 60%, gray);
    background: color-mix(in srgb, var(--debase__color__primary) 60%, gray);
    filter: contrast(0.8) brightness(0.9) saturate(0.7);
    box-shadow: none;
  }
`;
 
const secondaryDisabledStyle = css`
  @layer debase {
    color: color-mix(in srgb, var(--debase__color__text) 60%, gray);
    outline: 1px solid color-mix(in srgb, var(--debase__color__line) 50%, gray);
    background: color-mix(in srgb, var(--debase__color__background) 70%, gray);
    filter: contrast(0.8) brightness(0.9) saturate(0.7);
  }
`;
 
const dangerDisabledStyle = css`
  @layer debase {
    color: color-mix(in srgb, var(--debase__color__danger-label) 60%, gray);
    background: color-mix(in srgb, var(--debase__color__danger) 60%, gray);
    filter: contrast(0.8) brightness(0.9) saturate(0.7);
    box-shadow: none;
  }
`;
 
const nakedDisabledStyle = css`
  @layer debase {
    color: color-mix(in srgb, var(--debase__color__text) 60%, gray);
    background: transparent;
    filter: contrast(0.8) brightness(0.9) saturate(0.7);
  }
`;

Use Cases

The Button component offers four distinct variants to fit different UI needs:

  • Primary: The main call-to-action button, used for primary actions on a page or form.
  • Secondary: Used for secondary actions that should be less visually dominant.
  • Danger: Indicates destructive actions like delete or remove.
  • Naked: A minimal button without background, useful for subtle actions or text links.

Multiple variants of the buttons:

With a loading state:

The loading state maintains the original width of the button to prevent layout shifts when transitioning between normal and loading states. This ensures a smooth UI experience, especially in forms or button groups where sudden width changes could disrupt the layout.

With a disabled state: