gluestack-components

$npx mdskill add TheBushidoCollective/han/gluestack-components

Build accessible React and React Native UI with gluestack-ui components.

  • Installs and composes 50+ unstyled components for web and mobile.
  • Uses NativeWind for styling and supports CLI initialization commands.
  • Applies consistent patterns for variants, sizes, and accessibility props.
  • Delivers copy-pasteable code ready for immediate integration.

SKILL.md

.github/skills/gluestack-componentsView on GitHub ↗
---
name: gluestack-components
user-invocable: false
description: Use when building UI with gluestack-ui components. Covers component composition, variants, sizes, states, accessibility props, and platform-specific considerations for React and React Native.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Grep
  - Glob
---

# gluestack-ui - Components

Expert knowledge of gluestack-ui's universal component library for building accessible, performant UI across React and React Native platforms.

## Overview

gluestack-ui provides 50+ unstyled, accessible components that work seamlessly on web and mobile. Components are copy-pasteable into your project and styled with NativeWind (Tailwind CSS for React Native).

## Key Concepts

### Component Installation

Add components using the CLI:

```bash
# Initialize gluestack-ui in your project
npx gluestack-ui init

# Add individual components
npx gluestack-ui add button
npx gluestack-ui add input
npx gluestack-ui add modal

# Add multiple components
npx gluestack-ui add button input select modal

# Add all components
npx gluestack-ui add --all
```

### Component Anatomy

Every gluestack-ui component follows a consistent pattern:

```tsx
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';

// Components are composable with sub-components
<Button size="md" variant="solid" action="primary">
  <ButtonIcon as={PlusIcon} />
  <ButtonText>Add Item</ButtonText>
  <ButtonSpinner />
</Button>
```

### Variants, Sizes, and Actions

Components support consistent prop APIs:

```tsx
// Button variants
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="link">Link</Button>

// Button sizes
<Button size="xs">Extra Small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>

// Button actions (semantic colors)
<Button action="primary">Primary</Button>
<Button action="secondary">Secondary</Button>
<Button action="positive">Positive</Button>
<Button action="negative">Negative</Button>
```

## Core Components

### Button

Interactive button with loading states and icons:

```tsx
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';
import { SaveIcon } from 'lucide-react-native';

function SaveButton({ isLoading, onPress }: { isLoading: boolean; onPress: () => void }) {
  return (
    <Button
      size="md"
      variant="solid"
      action="primary"
      isDisabled={isLoading}
      onPress={onPress}
    >
      {isLoading ? (
        <ButtonSpinner />
      ) : (
        <ButtonIcon as={SaveIcon} />
      )}
      <ButtonText>{isLoading ? 'Saving...' : 'Save'}</ButtonText>
    </Button>
  );
}
```

### Input

Text input with labels and validation:

```tsx
import {
  FormControl,
  FormControlLabel,
  FormControlLabelText,
  FormControlHelper,
  FormControlHelperText,
  FormControlError,
  FormControlErrorIcon,
  FormControlErrorText,
} from '@/components/ui/form-control';
import { Input, InputField, InputSlot, InputIcon } from '@/components/ui/input';
import { AlertCircleIcon, MailIcon } from 'lucide-react-native';

function EmailInput({ value, onChange, error }: {
  value: string;
  onChange: (text: string) => void;
  error?: string;
}) {
  return (
    <FormControl isInvalid={!!error}>
      <FormControlLabel>
        <FormControlLabelText>Email</FormControlLabelText>
      </FormControlLabel>
      <Input variant="outline" size="md">
        <InputSlot pl="$3">
          <InputIcon as={MailIcon} />
        </InputSlot>
        <InputField
          placeholder="Enter your email"
          value={value}
          onChangeText={onChange}
          keyboardType="email-address"
          autoCapitalize="none"
        />
      </Input>
      {error ? (
        <FormControlError>
          <FormControlErrorIcon as={AlertCircleIcon} />
          <FormControlErrorText>{error}</FormControlErrorText>
        </FormControlError>
      ) : (
        <FormControlHelper>
          <FormControlHelperText>We'll never share your email</FormControlHelperText>
        </FormControlHelper>
      )}
    </FormControl>
  );
}
```

### Select

Dropdown selection component:

```tsx
import {
  Select,
  SelectTrigger,
  SelectInput,
  SelectIcon,
  SelectPortal,
  SelectBackdrop,
  SelectContent,
  SelectDragIndicatorWrapper,
  SelectDragIndicator,
  SelectItem,
} from '@/components/ui/select';
import { ChevronDownIcon } from 'lucide-react-native';

function CountrySelect({ value, onValueChange }: {
  value: string;
  onValueChange: (value: string) => void;
}) {
  return (
    <Select selectedValue={value} onValueChange={onValueChange}>
      <SelectTrigger variant="outline" size="md">
        <SelectInput placeholder="Select country" />
        <SelectIcon as={ChevronDownIcon} mr="$3" />
      </SelectTrigger>
      <SelectPortal>
        <SelectBackdrop />
        <SelectContent>
          <SelectDragIndicatorWrapper>
            <SelectDragIndicator />
          </SelectDragIndicatorWrapper>
          <SelectItem label="United States" value="us" />
          <SelectItem label="Canada" value="ca" />
          <SelectItem label="United Kingdom" value="uk" />
          <SelectItem label="Australia" value="au" />
        </SelectContent>
      </SelectPortal>
    </Select>
  );
}
```

### Modal

Dialog overlay component:

```tsx
import {
  Modal,
  ModalBackdrop,
  ModalContent,
  ModalHeader,
  ModalCloseButton,
  ModalBody,
  ModalFooter,
} from '@/components/ui/modal';
import { Heading } from '@/components/ui/heading';
import { Text } from '@/components/ui/text';
import { Button, ButtonText } from '@/components/ui/button';
import { CloseIcon, Icon } from '@/components/ui/icon';

function ConfirmModal({ isOpen, onClose, onConfirm, title, message }: {
  isOpen: boolean;
  onClose: () => void;
  onConfirm: () => void;
  title: string;
  message: string;
}) {
  return (
    <Modal isOpen={isOpen} onClose={onClose} size="md">
      <ModalBackdrop />
      <ModalContent>
        <ModalHeader>
          <Heading size="lg">{title}</Heading>
          <ModalCloseButton>
            <Icon as={CloseIcon} />
          </ModalCloseButton>
        </ModalHeader>
        <ModalBody>
          <Text>{message}</Text>
        </ModalBody>
        <ModalFooter>
          <Button variant="outline" action="secondary" onPress={onClose}>
            <ButtonText>Cancel</ButtonText>
          </Button>
          <Button action="negative" onPress={onConfirm}>
            <ButtonText>Delete</ButtonText>
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
}
```

### Toast

Notification component:

```tsx
import {
  Toast,
  ToastTitle,
  ToastDescription,
  useToast,
} from '@/components/ui/toast';

function NotificationExample() {
  const toast = useToast();

  const showToast = () => {
    toast.show({
      placement: 'top',
      render: ({ id }) => (
        <Toast nativeID={`toast-${id}`} action="success" variant="solid">
          <ToastTitle>Success!</ToastTitle>
          <ToastDescription>Your changes have been saved.</ToastDescription>
        </Toast>
      ),
    });
  };

  return (
    <Button onPress={showToast}>
      <ButtonText>Show Toast</ButtonText>
    </Button>
  );
}
```

### Accordion

Expandable content sections:

```tsx
import {
  Accordion,
  AccordionItem,
  AccordionHeader,
  AccordionTrigger,
  AccordionTitleText,
  AccordionIcon,
  AccordionContent,
  AccordionContentText,
} from '@/components/ui/accordion';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react-native';

function FAQAccordion({ items }: { items: { question: string; answer: string }[] }) {
  return (
    <Accordion type="multiple" defaultValue={['item-0']}>
      {items.map((item, index) => (
        <AccordionItem key={index} value={`item-${index}`}>
          <AccordionHeader>
            <AccordionTrigger>
              {({ isExpanded }) => (
                <>
                  <AccordionTitleText>{item.question}</AccordionTitleText>
                  <AccordionIcon as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
                </>
              )}
            </AccordionTrigger>
          </AccordionHeader>
          <AccordionContent>
            <AccordionContentText>{item.answer}</AccordionContentText>
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  );
}
```

### Checkbox and Radio

Selection controls:

```tsx
import {
  Checkbox,
  CheckboxIndicator,
  CheckboxIcon,
  CheckboxLabel,
} from '@/components/ui/checkbox';
import {
  RadioGroup,
  Radio,
  RadioIndicator,
  RadioIcon,
  RadioLabel,
} from '@/components/ui/radio';
import { CheckIcon, CircleIcon } from 'lucide-react-native';

function TermsCheckbox({ isChecked, onChange }: {
  isChecked: boolean;
  onChange: (checked: boolean) => void;
}) {
  return (
    <Checkbox value="terms" isChecked={isChecked} onChange={onChange}>
      <CheckboxIndicator>
        <CheckboxIcon as={CheckIcon} />
      </CheckboxIndicator>
      <CheckboxLabel>I agree to the terms and conditions</CheckboxLabel>
    </Checkbox>
  );
}

function ShippingOptions({ value, onChange }: {
  value: string;
  onChange: (value: string) => void;
}) {
  return (
    <RadioGroup value={value} onChange={onChange}>
      <Radio value="standard">
        <RadioIndicator>
          <RadioIcon as={CircleIcon} />
        </RadioIndicator>
        <RadioLabel>Standard Shipping (5-7 days)</RadioLabel>
      </Radio>
      <Radio value="express">
        <RadioIndicator>
          <RadioIcon as={CircleIcon} />
        </RadioIndicator>
        <RadioLabel>Express Shipping (2-3 days)</RadioLabel>
      </Radio>
      <Radio value="overnight">
        <RadioIndicator>
          <RadioIcon as={CircleIcon} />
        </RadioIndicator>
        <RadioLabel>Overnight Shipping (1 day)</RadioLabel>
      </Radio>
    </RadioGroup>
  );
}
```

## Best Practices

### 1. Use Composition Over Configuration

Compose components with sub-components for flexibility:

```tsx
// Good: Composable structure
<Button>
  <ButtonIcon as={PlusIcon} />
  <ButtonText>Add</ButtonText>
</Button>

// Avoid: Prop-heavy configuration
<Button icon={PlusIcon} text="Add" iconPosition="left" />
```

### 2. Leverage FormControl for Form Fields

Wrap inputs in FormControl for consistent validation:

```tsx
<FormControl isRequired isInvalid={!!error}>
  <FormControlLabel>
    <FormControlLabelText>Password</FormControlLabelText>
  </FormControlLabel>
  <Input>
    <InputField type="password" />
  </Input>
  <FormControlError>
    <FormControlErrorText>{error}</FormControlErrorText>
  </FormControlError>
</FormControl>
```

### 3. Handle Platform Differences

Use platform-specific logic when needed:

```tsx
import { Platform } from 'react-native';

function ResponsiveModal({ children }: { children: React.ReactNode }) {
  return (
    <Modal size={Platform.OS === 'web' ? 'lg' : 'full'}>
      <ModalContent>
        {children}
      </ModalContent>
    </Modal>
  );
}
```

### 4. Use Proper Loading States

Show loading feedback for async operations:

```tsx
function SubmitButton({ isLoading, onPress }: {
  isLoading: boolean;
  onPress: () => void;
}) {
  return (
    <Button isDisabled={isLoading} onPress={onPress}>
      {isLoading && <ButtonSpinner mr="$2" />}
      <ButtonText>{isLoading ? 'Submitting...' : 'Submit'}</ButtonText>
    </Button>
  );
}
```

### 5. Create Reusable Component Wrappers

Build app-specific components on top of gluestack-ui:

```tsx
// components/app/PrimaryButton.tsx
import { Button, ButtonText, ButtonSpinner } from '@/components/ui/button';

interface PrimaryButtonProps {
  children: string;
  isLoading?: boolean;
  onPress: () => void;
}

export function PrimaryButton({ children, isLoading, onPress }: PrimaryButtonProps) {
  return (
    <Button
      size="lg"
      variant="solid"
      action="primary"
      isDisabled={isLoading}
      onPress={onPress}
      className="rounded-full"
    >
      {isLoading && <ButtonSpinner mr="$2" />}
      <ButtonText>{children}</ButtonText>
    </Button>
  );
}
```

## Common Patterns

### Form with Validation

```tsx
import { useState } from 'react';
import { VStack } from '@/components/ui/vstack';
import { Button, ButtonText } from '@/components/ui/button';
import { FormControl, FormControlError, FormControlErrorText } from '@/components/ui/form-control';
import { Input, InputField } from '@/components/ui/input';

interface FormData {
  email: string;
  password: string;
}

interface FormErrors {
  email?: string;
  password?: string;
}

function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
  const [formData, setFormData] = useState<FormData>({ email: '', password: '' });
  const [errors, setErrors] = useState<FormErrors>({});

  const validate = (): boolean => {
    const newErrors: FormErrors = {};

    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Invalid email format';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = () => {
    if (validate()) {
      onSubmit(formData);
    }
  };

  return (
    <VStack space="md">
      <FormControl isInvalid={!!errors.email}>
        <Input>
          <InputField
            placeholder="Email"
            value={formData.email}
            onChangeText={(text) => setFormData({ ...formData, email: text })}
          />
        </Input>
        <FormControlError>
          <FormControlErrorText>{errors.email}</FormControlErrorText>
        </FormControlError>
      </FormControl>

      <FormControl isInvalid={!!errors.password}>
        <Input>
          <InputField
            placeholder="Password"
            type="password"
            value={formData.password}
            onChangeText={(text) => setFormData({ ...formData, password: text })}
          />
        </Input>
        <FormControlError>
          <FormControlErrorText>{errors.password}</FormControlErrorText>
        </FormControlError>
      </FormControl>

      <Button onPress={handleSubmit}>
        <ButtonText>Login</ButtonText>
      </Button>
    </VStack>
  );
}
```

### Data List with Actions

```tsx
import { HStack } from '@/components/ui/hstack';
import { VStack } from '@/components/ui/vstack';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Heading } from '@/components/ui/heading';
import { Button, ButtonIcon } from '@/components/ui/button';
import { Pressable } from '@/components/ui/pressable';
import { TrashIcon, EditIcon } from 'lucide-react-native';

interface Item {
  id: string;
  title: string;
  description: string;
}

function ItemList({ items, onEdit, onDelete }: {
  items: Item[];
  onEdit: (id: string) => void;
  onDelete: (id: string) => void;
}) {
  return (
    <VStack space="sm">
      {items.map((item) => (
        <Box
          key={item.id}
          className="p-4 bg-background-50 rounded-lg border border-outline-200"
        >
          <HStack justifyContent="space-between" alignItems="center">
            <VStack space="xs" flex={1}>
              <Heading size="sm">{item.title}</Heading>
              <Text size="sm" className="text-typography-500">
                {item.description}
              </Text>
            </VStack>
            <HStack space="xs">
              <Button
                size="sm"
                variant="outline"
                action="secondary"
                onPress={() => onEdit(item.id)}
              >
                <ButtonIcon as={EditIcon} />
              </Button>
              <Button
                size="sm"
                variant="outline"
                action="negative"
                onPress={() => onDelete(item.id)}
              >
                <ButtonIcon as={TrashIcon} />
              </Button>
            </HStack>
          </HStack>
        </Box>
      ))}
    </VStack>
  );
}
```

## Anti-Patterns

### Do Not Mix Styling Approaches

```tsx
// Bad: Mixing inline styles with NativeWind
<Button style={{ backgroundColor: 'blue' }} className="p-4">
  <ButtonText>Click</ButtonText>
</Button>

// Good: Use NativeWind classes consistently
<Button className="bg-blue-500 p-4">
  <ButtonText>Click</ButtonText>
</Button>
```

### Do Not Skip Accessibility Props

```tsx
// Bad: Missing accessibility information
<Pressable onPress={handlePress}>
  <Icon as={MenuIcon} />
</Pressable>

// Good: Include accessibility props
<Pressable
  onPress={handlePress}
  accessibilityLabel="Open menu"
  accessibilityRole="button"
>
  <Icon as={MenuIcon} />
</Pressable>
```

### Do Not Ignore Platform Constraints

```tsx
// Bad: Using web-only APIs on native
<Modal>
  <ModalContent onClick={handleClick}> {/* onClick doesn't work on native */}
    ...
  </ModalContent>
</Modal>

// Good: Use cross-platform events
<Modal>
  <ModalContent>
    <Pressable onPress={handlePress}>
      ...
    </Pressable>
  </ModalContent>
</Modal>
```

### Do Not Hardcode Colors

```tsx
// Bad: Hardcoded colors
<Box className="bg-[#3B82F6]">
  <Text className="text-[#FFFFFF]">Hello</Text>
</Box>

// Good: Use theme tokens
<Box className="bg-primary-500">
  <Text className="text-typography-0">Hello</Text>
</Box>
```

## Related Skills

- **gluestack-theming**: Customizing themes and design tokens
- **gluestack-accessibility**: Ensuring accessible implementations

More from TheBushidoCollective/han

SkillDescription
absinthe-resolversUse when implementing GraphQL resolvers with Absinthe. Covers resolver patterns, dataloader integration, batching, and error handling.
absinthe-schemaUse when designing GraphQL schemas with Absinthe. Covers type definitions, interfaces, unions, enums, and schema organization patterns.
absinthe-subscriptionsUse when implementing real-time GraphQL subscriptions with Absinthe. Covers Phoenix channels, PubSub, and subscription patterns.
act-docker-setupUse when configuring Docker environments for act, selecting runner images, managing container resources, or troubleshooting Docker-related issues with local GitHub Actions testing.
act-local-testingUse when testing GitHub Actions workflows locally with act. Covers act CLI usage, Docker configuration, debugging workflows, and troubleshooting common issues when running workflows on your local machine.
act-workflow-syntaxUse when creating or modifying GitHub Actions workflow files. Provides guidance on workflow syntax, triggers, jobs, steps, and expressions for creating valid GitHub Actions workflows that can be tested locally with act.
ameba-configurationUse when configuring Ameba rules and settings for Crystal projects including .ameba.yml setup, rule management, severity levels, and code quality enforcement.
ameba-custom-rulesUse when creating custom Ameba rules for Crystal code analysis including rule development, AST traversal, issue reporting, and rule testing.
ameba-integrationUse when integrating Ameba into development workflows including CI/CD pipelines, pre-commit hooks, GitHub Actions, and automated code review processes.
analyze-performanceAnalyze performance metrics and identify slow transactions in Sentry