Button Component

Interactive playground to experiment with button variants, icons, text customization, and disabled states

Buttons icons

Button with spinner

Documentation

Button Component

The Button component provides a Material Design 3 compliant button control that allows users to perform actions with a single tap or click. It's designed to be lightweight, accessible, and highly customizable to fit various UI contexts.

Overview

Buttons are commonly used for:

  • Submitting forms
  • Triggering actions
  • Navigation
  • Dialog controls
  • Toolbars and action menus

The component follows Material Design 3 guidelines with support for various variants (filled, outlined, text, etc.), shapes, icons, disabled states, progress indicators, and ripple effects.

Import

import { createButton } from 'mtrl';

Basic Usage

// Create a basic button
const submitButton = createButton({
  text: 'Submit',
  variant: 'filled'
});

// Add to your page
document.querySelector('.form-actions').appendChild(submitButton.element);

// Listen for clicks
submitButton.on('click', () => {
  console.log('Button clicked');
});

Configuration

The Button component accepts the following configuration options:

OptionTypeDefaultDescription
variantstring'filled'Visual style of the button (filled, outlined, text, elevated, tonal)
sizestring's'Size of the button (xs, s, m, l, xl)
shapestring'round'Shape of the button (round, square)
disabledbooleanfalseWhether the button is initially disabled
textstringundefinedText content displayed inside the button
iconstringundefinedHTML content (typically SVG) for the button icon
iconSizestringundefinedSize of the icon (e.g., '18px')
classstringundefinedAdditional CSS classes to add to the button
valuestringundefinedButton value attribute
typestring'button'Button type attribute (button, submit, reset)
ripplebooleantrueWhether to enable ripple effect on interaction
prefixstring'mtrl'Prefix for CSS class names
rippleConfigobjectundefinedConfiguration options for the ripple effect
ariaLabelstringundefinedARIA label for accessibility (important for icon-only buttons)
progressboolean\|objectundefinedProgress indicator configuration
showProgressbooleanfalseWhether to show progress initially

Button Variants

The button supports 5 Material Design 3 variants:

  • filled (default): Primary action button with solid background (high emphasis)
  • tonal: Secondary action button with medium emphasis
  • outlined: Button with outline border and transparent background
  • elevated: Button with slight elevation/shadow
  • text: Button that appears as text without background or border (low emphasis)

Button Sizes

The button supports 5 different sizes following Material Design 3 specifications:

  • xs: Extra small - 32px height
  • s: Small - 40px height (default)
  • m: Medium - 56px height
  • l: Large - 96px height
  • xl: Extra large - 136px height

Button Shapes

The button supports 2 different shapes:

  • round (default): Pill-shaped buttons with fully rounded corners
  • square: Buttons with size-specific corner radius (small radius that scales with button size)

Component API

The Button component provides the following methods:

Value Methods

MethodParametersReturnsDescription
getValue()nonestringGets the button's current value attribute
setValue(value)value: stringButtonComponentSets the button's value attribute

State Methods

MethodParametersReturnsDescription
enable()noneButtonComponentEnables the button, making it interactive
disable()noneButtonComponentDisables the button, making it non-interactive
setActive(active)active: booleanButtonComponentSets the active state of the button (e.g., when a related menu is open)

Variant Methods

MethodParametersReturnsDescription
setVariant(variant)variant: stringButtonComponentChanges the button's visual style variant
getVariant()nonestringGets the button's current variant

Size Methods

MethodParametersReturnsDescription
setSize(size)size: stringButtonComponentSets the button's size (xs, s, m, l, xl)
getSize()nonestringGets the button's current size

Shape Methods

MethodParametersReturnsDescription
setShape(shape)shape: stringButtonComponentSets the button's shape (round, square)
getShape()nonestringGets the button's current shape

Content Methods

MethodParametersReturnsDescription
setText(content)content: stringButtonComponentSets the button's text content
getText()nonestringGets the button's current text content
setIcon(icon)icon: stringButtonComponentSets the button's icon HTML content (empty string removes icon)
getIcon()nonestringGets the button's current icon HTML content
hasIcon()nonebooleanChecks if the button has an icon
setAriaLabel(label)label: stringButtonComponentSets the button's aria-label attribute for accessibility

Progress Methods (when progress is configured)

MethodParametersReturnsDescription
showProgress()nonePromise<ButtonComponent>Shows the progress indicator
showProgressSync()noneButtonComponentShows the progress indicator synchronously
hideProgress()nonePromise<ButtonComponent>Hides the progress indicator
hideProgressSync()noneButtonComponentHides the progress indicator synchronously
setProgress(value)value: numberPromise<ButtonComponent>Sets progress value (0-100)
setProgressSync(value)value: numberButtonComponentSets progress value synchronously
setIndeterminate(indeterminate)indeterminate: booleanPromise<ButtonComponent>Sets indeterminate mode
setIndeterminateSync(indeterminate)indeterminate: booleanButtonComponentSets indeterminate mode synchronously
setLoading(loading, text?)loading: boolean, text?: stringPromise<ButtonComponent>Sets loading state with optional text
setLoadingSync(loading, text?)loading: boolean, text?: stringButtonComponentSets loading state synchronously

Event Methods

MethodParametersReturnsDescription
on(event, handler)event: string, handler: FunctionButtonComponentAdds an event listener
off(event, handler)event: string, handler: FunctionButtonComponentRemoves an event listener

Style Methods

MethodParametersReturnsDescription
addClass(...classes)...classes: string[]ButtonComponentAdds CSS classes to the button element

Lifecycle Methods

MethodParametersReturnsDescription
destroy()nonevoidDestroys the button component and cleans up resources

Events

The Button component emits the following events:

EventDescriptionData
clickFires when the button is clicked{ event: MouseEvent }
focusFires when the button receives focus{ event: FocusEvent }
blurFires when the button loses focus{ event: FocusEvent }

Examples

Basic Button Variants

// Filled button (default)
const filledButton = createButton({
  text: 'Filled Button'
});

// Outlined button
const outlinedButton = createButton({
  text: 'Outlined Button',
  variant: 'outlined'
});

// Text button
const textButton = createButton({
  text: 'Text Button',
  variant: 'text'
});

// Elevated button
const elevatedButton = createButton({
  text: 'Elevated Button',
  variant: 'elevated'
});

// Tonal button
const tonalButton = createButton({
  text: 'Tonal Button',
  variant: 'tonal'
});

Button Sizes

// Extra small button
const xsButton = createButton({
  text: 'XS',
  size: 'xs'
});

// Small button (default)
const smallButton = createButton({
  text: 'Small'
});

// Medium button
const mediumButton = createButton({
  text: 'Medium',
  size: 'm'
});

// Large button
const largeButton = createButton({
  text: 'Large',
  size: 'l'
});

// Extra large button
const xlButton = createButton({
  text: 'Extra Large',
  size: 'xl'
});

Button Shapes

// Round button (default)
const roundButton = createButton({
  text: 'Round Button',
  shape: 'round'
});

// Square button
const squareButton = createButton({
  text: 'Square Button',
  shape: 'square'
});

// Square buttons work well with different sizes
const squareSmall = createButton({
  text: 'Small Square',
  shape: 'square',
  size: 's'
});

const squareLarge = createButton({
  text: 'Large Square',
  shape: 'square',
  size: 'l'
});

Button with Icon

const saveButton = createButton({
  text: 'Save',
  icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3.5 7 8 15 8"></polyline></svg>'
});

Icon-Only Button

const addButton = createButton({
  icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>',
  ariaLabel: 'Add item'  // Important for accessibility
});

Dynamic Icon Management

const toggleButton = createButton({
  text: 'Toggle',
  icon: '▶️'
});

toggleButton.on('click', () => {
  if (toggleButton.hasIcon()) {
    // Remove the icon
    toggleButton.setIcon('');
  } else {
    // Add the icon back
    toggleButton.setIcon('⏸️');
  }
});

Form Submit Button

const submitButton = createButton({
  text: 'Submit',
  type: 'submit'
});

const form = document.querySelector('form');
form.appendChild(submitButton.element);

Disabled Button

const disabledButton = createButton({
  text: 'Not Available',
  disabled: true
});

Button with Event Handling

const actionButton = createButton({
  text: 'Load More'
});

actionButton.on('click', async () => {
  actionButton.disable();
  actionButton.setText('Loading...');
  
  try {
    await loadMoreItems();
    actionButton.setText('Load More');
  } catch (error) {
    console.error('Failed to load items:', error);
    actionButton.setText('Retry');
  } finally {
    actionButton.enable();
  }
});

Dynamically Changing Button Properties

const toggleButton = createButton({
  text: 'Normal State',
  variant: 'text',
  shape: 'round',
  size: 's'
});

toggleButton.on('click', () => {
  if (toggleButton.getVariant() === 'text') {
    toggleButton.setVariant('filled');
    toggleButton.setShape('square');
    toggleButton.setSize('m');
    toggleButton.setText('Active State');
  } else {
    toggleButton.setVariant('text');
    toggleButton.setShape('round');
    toggleButton.setSize('s');
    toggleButton.setText('Normal State');
  }
});

Button Progress Feature

The button component includes built-in progress indicator support that is lazily loaded - meaning the progress component is only imported and initialized when actually used.

Benefits

  • Zero overhead: Buttons without progress configuration don't import any progress code
  • Automatic code splitting: Progress component is dynamically imported only when needed
  • Seamless integration: Progress appears as the button's icon
  • Flexible API: Both synchronous and asynchronous methods available

Basic Usage

Enable Progress

// Simple boolean to enable with defaults
const button1 = createButton({
  text: 'Submit',
  progress: true  // Uses default circular progress
});

// Or with custom configuration
const button2 = createButton({
  text: 'Upload',
  progress: {
    variant: 'circular',
    size: 20,
    indeterminate: false
  }
});

Synchronous API (Recommended for UI)

For most UI interactions, use the synchronous methods:

button.on('click', () => {
  // Show loading state
  button.setLoadingSync(true, 'Processing...');
  
  // Do async work
  fetch('/api/data')
    .then(response => {
      // Update progress
      button.setProgressSync(75);
      return response.json();
    })
    .then(data => {
      // Complete
      button.setLoadingSync(false, 'Success!');
    });
});

Asynchronous API

Use async methods when you need to ensure the progress is loaded:

button.on('click', async () => {
  // This ensures progress is loaded before continuing
  await button.setLoading(true, 'Starting...');
  
  for (let i = 0; i <= 100; i += 10) {
    await button.setProgress(i);
    await delay(200);
  }
  
  await button.setLoading(false, 'Complete');
});

Progress Configuration

When using progress: true, default configuration is applied:

{
  variant: 'circular',
  size: 20,
  thickness: 2,
  indeterminate: true
}

For custom configuration:

const button = createButton({
  text: 'Upload',
  progress: {
    variant: 'circular',    // 'circular' or 'linear'
    size: 24,              // Size in pixels
    thickness: 3,          // Thickness of progress ring/bar
    indeterminate: false,  // Whether to show indeterminate progress
    color: 'primary'       // Color variant
  }
});

Progress Examples

File Upload with Progress

const uploadBtn = createButton({
  text: 'Upload File',
  progress: { variant: 'circular', size: 18 }
});

uploadBtn.on('click', () => {
  const file = getSelectedFile();
  
  uploadBtn.setLoadingSync(true, 'Uploading...');
  
  uploadFile(file, (progress) => {
    uploadBtn.setProgressSync(progress);
  }).then(() => {
    uploadBtn.setLoadingSync(false, 'Uploaded!');
  });
});

Form Submission

const submitBtn = createButton({
  text: 'Submit Form',
  type: 'submit',
  progress: true
});

form.on('submit', async (e) => {
  e.preventDefault();
  
  submitBtn.setLoadingSync(true, 'Submitting...');
  
  try {
    await submitForm(form.getData());
    submitBtn.setLoadingSync(false, 'Success!');
  } catch (error) {
    submitBtn.setLoadingSync(false, 'Try Again');
  }
});

Multi-step Process

const processBtn = createButton({
  text: 'Start Process',
  progress: { indeterminate: false }
});

processBtn.on('click', async () => {
  const steps = ['Initializing', 'Processing', 'Finalizing'];
  
  processBtn.setLoadingSync(true, steps[0]);
  processBtn.setProgressSync(0);
  
  for (let i = 0; i < steps.length; i++) {
    processBtn.setText(steps[i]);
    
    // Simulate work
    await performStep(i);
    
    const progress = ((i + 1) / steps.length) * 100;
    processBtn.setProgressSync(progress);
  }
  
  processBtn.setLoadingSync(false, 'Complete!');
});

Performance

The progress component is only loaded when:

  • A button is created with progress configuration AND
  • One of the progress methods is called OR showProgress: true is set

This means:

  • Buttons without progress have zero overhead
  • The progress component code is automatically code-split
  • First usage may have a tiny delay while loading (usually imperceptible)

Progress Styling

The progress indicator automatically adapts to the button variant's color scheme:

  • Filled buttons: Progress uses on-primary color
  • Elevated buttons: Progress uses primary color
  • Tonal buttons: Progress uses on-secondary-container color
  • Outlined buttons: Progress uses primary color
  • Text buttons: Progress uses primary color

Additional styling features:

  • Progress smoothly fades in/out with transitions
  • Circular buttons show larger progress indicators (24px)
  • Linear progress is sized appropriately for buttons (48px × 3px)
  • Progress remains visible in disabled state with reduced opacity
  • Dark theme is fully supported with appropriate color adjustments

Functional Composition

The Button component is built using functional composition, combining multiple features:

Core Features

  • Base Component (createBase): Provides the foundation with component creation utilities.
  • Element Creation (withElement): Creates the DOM element with proper attributes and classes.
  • Event Handling (withEvents): Enables event listening and emission.
  • Text Content (withText): Manages text content within the button.
  • Icon Support (withIcon): Adds and manages icon elements.
  • Variant Styling (withVariant): Applies visual styling variants like filled, outlined, etc.
  • Size Styling (withSize): Applies size variants (xs, s, m, l, xl).
  • Disabled State (withDisabled): Manages the disabled state of the button.
  • Progress Indicators (withProgress): Adds lazily-loaded progress functionality.
  • Ripple Effect (withRipple): Adds Material Design ripple feedback effect.
  • Lifecycle Management (withLifecycle): Handles component lifecycle including destruction.
  • Public API (withAPI): Exposes a clean, chainable API for users.

How Composition Works

The button component is created by "piping" these features together:

const button = pipe(
  createBase,               // Start with base component
  withEvents(),             // Add event capability
  withElement(config),      // Create DOM element
  withVariant(config),      // Apply variant styling
  withSize(config),         // Apply size styling
  withText(config),         // Add text content
  withIcon(config),         // Add icon support
  withDisabled(config),     // Add disabled state
  withProgress(config),     // Add progress functionality
  withRipple(config),       // Add ripple effect
  withLifecycle(),          // Add lifecycle management
  comp => withAPI(config)(comp)  // Apply public API
)(baseConfig);

This composition pattern allows for:

  • Modular, testable code
  • Clean separation of concerns
  • Lightweight bundles (only include what you need)
  • Easy extension and customization

Accessibility

The Button component follows accessibility best practices:

  • Proper semantic HTML with <button> element
  • Support for aria-label attribute for icon-only buttons
  • Keyboard navigation and focus handling
  • Visual focus indicators
  • Disabled states properly communicated to screen readers
  • Progress states announced to screen readers

Keyboard Navigation

KeyAction
TabMoves focus to the button
Space or EnterActivates the button

CSS Customization

The Button component uses BEM-style CSS classes for easy customization:

/* Base button styles */
.mtrl-button { /* ... */ }

/* Button variants */
.mtrl-button--filled { /* ... */ }
.mtrl-button--outlined { /* ... */ }
.mtrl-button--text { /* ... */ }
.mtrl-button--elevated { /* ... */ }
.mtrl-button--tonal { /* ... */ }

/* Button sizes */
.mtrl-button--xs { /* ... */ }
.mtrl-button--s { /* ... */ }
.mtrl-button--m { /* ... */ }
.mtrl-button--l { /* ... */ }
.mtrl-button--xl { /* ... */ }

/* Button shapes */
.mtrl-button--round { /* ... */ }
.mtrl-button--square { /* ... */ }

/* Button states */
.mtrl-button--disabled { /* ... */ }
.mtrl-button--active { /* ... */ }
.mtrl-button--progress { /* ... */ }

/* Button with icon */
.mtrl-button--icon { /* ... */ }
.mtrl-button-icon { /* ... */ }
.mtrl-button-text { /* ... */ }

/* Circular icon-only button */
.mtrl-button--circular { /* ... */ }

/* Progress integration */
.mtrl-button-progress { /* ... */ }

/* Ripple effect */
.mtrl-ripple { /* ... */ }
.mtrl-ripple-wave { /* ... */ }

Best Practices

  • Use the appropriate button variant for each context

- Filled (primary actions)

- Outlined (secondary actions)

- Text (low-emphasis actions)

  • Choose the right size for your context

- XS/S for compact interfaces

- M for standard interfaces

- L/XL for touch-first or prominent actions

  • Use square shape for buttons that need to align with other square elements
  • Provide clear, concise text labels
  • Use icons to enhance clarity, not to replace text (except for well-known actions)
  • Always include aria-label for icon-only buttons
  • Place primary actions on the right in dialogs and forms
  • Don't use too many prominent buttons in a single view
  • Keep button text to 1-3 words when possible
  • Use progress indicators for operations that take longer than 1 second

Performance Considerations

The Button component is designed to be lightweight and performant:

  • Minimal DOM operations
  • Efficient event handling
  • Optimized ripple animations
  • Lazy-loaded progress functionality
  • Clean destruction to prevent memory leaks
  • No external dependencies