UTA DevHub

Button Component

Primary action component with multiple variants, sizes, and states

Button Component

Overview

The Button component is a foundational interactive element that triggers actions when tapped. It supports multiple visual variants, sizes, loading states, and full accessibility features.

GitHub Source: View the complete implementation in our repository.

Basic Usage

import { Button } from '@/ui/foundation/Button';
 
// Primary button
<Button onPress={handlePress}>
  Save Changes
</Button>
 
// With variant
<Button variant="secondary" onPress={handlePress}>
  Cancel
</Button>
 
// With loading state
<Button loading onPress={handleSubmit}>
  Submit
</Button>

Variants

Our Button component supports multiple visual variants for different use cases:

<Button variant="primary" onPress={handlePress}>
  Primary Action
</Button>

Primary buttons are used for the main action on a screen. They use the primary brand color and should be used sparingly - typically one per screen or section.

Sizes

Buttons come in three sizes to fit different contexts:

// Small - for compact spaces
<Button size="small" onPress={handlePress}>
  Small
</Button>
 
// Medium - default size
<Button size="medium" onPress={handlePress}>
  Medium
</Button>
 
// Large - for prominent CTAs
<Button size="large" onPress={handlePress}>
  Large
</Button>

States

Loading State

Show a loading indicator when performing async operations:

const [loading, setLoading] = useState(false);
 
const handleSubmit = async () => {
  setLoading(true);
  try {
    await submitForm();
  } finally {
    setLoading(false);
  }
};
 
<Button loading={loading} onPress={handleSubmit}>
  Submit Form
</Button>

Disabled State

Disable buttons when actions aren't available:

<Button 
  disabled={!isFormValid}
  onPress={handleSubmit}
>
  Submit
</Button>

Full Example

Here's a complete example showing all features:

import React, { useState } from 'react';
import { View } from 'react-native';
import { Button } from '@/ui/foundation/Button';
import { spacing } from '@/core/shared/styles/spacing';
 
export function ButtonShowcase() {
  const [loading, setLoading] = useState(false);
  
  const handleAsyncAction = async () => {
    setLoading(true);
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      console.log('Action completed!');
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <View style={{ gap: spacing.md }}>
      {/* Primary actions */}
      <Button 
        variant="primary"
        size="large"
        loading={loading}
        onPress={handleAsyncAction}
      >
        Save Changes
      </Button>
      
      {/* Secondary actions */}
      <Button 
        variant="secondary"
        onPress={() => console.log('Cancelled')}
      >
        Cancel
      </Button>
      
      {/* Tertiary actions */}
      <Button 
        variant="ghost"
        size="small"
        onPress={() => console.log('Learn more')}
      >
        Learn More
      </Button>
      
      {/* Destructive actions */}
      <Button 
        variant="danger"
        disabled={loading}
        onPress={() => console.log('Deleted')}
      >
        Delete Account
      </Button>
    </View>
  );
}

Props API

PropTypeDefaultDescription
childrenReactNoderequiredButton label text
variant'primary' | 'secondary' | 'ghost' | 'danger''primary'Visual style variant
size'small' | 'medium' | 'large''medium'Button size
loadingbooleanfalseShows loading indicator
disabledbooleanfalseDisables interaction
onPress() => void-Press event handler
styleViewStyle-Custom style overrides
testIDstring-Test identifier

Accessibility

The Button component includes comprehensive accessibility features:

  • Role: Automatically sets accessibilityRole="button"
  • State: Communicates disabled and loading states
  • Label: Uses button text as accessibility label
  • Feedback: Provides haptic feedback on press (when enabled)
<Button
  onPress={handlePress}
  accessibilityLabel="Save your changes"
  accessibilityHint="Double tap to save the form"
>
  Save
</Button>

Styling

Buttons use design tokens for consistent styling:

// ✅ GOOD - Component uses design tokens internally
<Button variant="primary">Click Me</Button>
 
// ✅ GOOD - Override with style prop when needed
<Button 
  variant="primary"
  style={{ marginTop: spacing.xl }}
>
  Click Me
</Button>
 
// ❌ BAD - Don't wrap in custom styled views
<View style={{ backgroundColor: 'blue', padding: 10 }}>
  <Button>Click Me</Button>
</View>

Common Patterns

Button Groups

Group related actions together:

<View style={{ flexDirection: 'row', gap: spacing.sm }}>
  <Button variant="ghost" onPress={handleCancel}>
    Cancel
  </Button>
  <Button variant="primary" onPress={handleSave}>
    Save
  </Button>
</View>

Confirmation Dialogs

Use with modals for confirmations:

<Modal visible={showConfirm} onClose={() => setShowConfirm(false)}>
  <Text>Are you sure you want to delete this item?</Text>
  <View style={{ flexDirection: 'row', gap: spacing.sm }}>
    <Button 
      variant="ghost" 
      onPress={() => setShowConfirm(false)}
    >
      Cancel
    </Button>
    <Button 
      variant="danger" 
      loading={deleting}
      onPress={handleDelete}
    >
      Delete
    </Button>
  </View>
</Modal>

Testing

Example test cases for Button component:

import { render, fireEvent } from '@testing-library/react-native';
import { Button } from '@/ui/foundation/Button';
 
describe('Button', () => {
  it('renders correctly', () => {
    const { getByText } = render(
      <Button onPress={jest.fn()}>Click Me</Button>
    );
    expect(getByText('Click Me')).toBeTruthy();
  });
  
  it('calls onPress when pressed', () => {
    const onPress = jest.fn();
    const { getByText } = render(
      <Button onPress={onPress}>Click Me</Button>
    );
    
    fireEvent.press(getByText('Click Me'));
    expect(onPress).toHaveBeenCalled();
  });
  
  it('does not call onPress when disabled', () => {
    const onPress = jest.fn();
    const { getByText } = render(
      <Button disabled onPress={onPress}>Click Me</Button>
    );
    
    fireEvent.press(getByText('Click Me'));
    expect(onPress).not.toHaveBeenCalled();
  });
});

Migration Guide

If migrating from a previous Button implementation:

// Old implementation
<TouchableOpacity style={styles.button} onPress={onPress}>
  <Text style={styles.buttonText}>Click Me</Text>
</TouchableOpacity>
 
// New implementation
<Button variant="primary" onPress={onPress}>
  Click Me
</Button>

Need help? Check the Foundation Components Guide or ask in #ui-development channel.

On this page