home / skills / gluestack / agent-skills / variants

This is most likely a fork of the gluestack-ui-v4 skill from gluestack
npx playbooks add skill gluestack/agent-skills --skill variants

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
23.6 KB
---
name: gluestack-ui-v4:variants
description: Guide for creating custom variants for gluestack-ui v4 components - covers tva usage, extending components, variant patterns, and customization strategies.
---

# Gluestack UI v4 - Creating Component Variants

This sub-skill focuses on creating custom variants for existing gluestack-ui v4 components, allowing you to extend the design system with project-specific styling patterns while maintaining consistency and type safety.

## When to Create a Variant

Create a new variant when:

1. **Repeating the same style combination** - Multiple places use the same className pattern
2. **Project-specific design patterns** - Brand-specific button styles, card types, etc.
3. **Conditional styling** - Component appearance changes based on props
4. **Extending existing components** - Adding new visual styles to Gluestack components
5. **Theme-specific variations** - Different appearances for specific contexts

**Don't create variants for:**
- One-off custom styles (use className instead)
- Simple modifications (use existing props + className)
- Styles that should be in the global design system

## Variant Creation Workflow

### Step 1: Analyze the Component

Before creating a variant, understand:

1. **What's the base component?** - Button, Card, Badge, etc.
2. **What visual states are needed?** - Colors, sizes, borders, shadows
3. **Are there sub-components?** - ButtonText, CardHeader, etc.
4. **What props should control variants?** - variant, size, state props
5. **Should variants affect children?** - Parent variants for sub-components

### Step 2: Plan Variant Structure

Define your variant system:

```tsx
// Example: Planning a Badge component with variants
{
  variant: ['default', 'success', 'warning', 'error', 'info']
  size: ['sm', 'md', 'lg']
  shape: ['rounded', 'pill', 'square']
}
```

### Step 3: Implement with tva

Use `tva` (Tailwind Variant Authority) to create type-safe, composable variants.

## Creating Simple Variants

### Template: Adding Variants to a Custom Component

```tsx
import React from 'react';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';

interface BadgeProps {
  readonly variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
  readonly size?: 'sm' | 'md' | 'lg';
  readonly shape?: 'rounded' | 'pill' | 'square';
  readonly className?: string;
  readonly children: React.ReactNode;
}

// Define variant styles
const badgeStyles = tva({
  base: 'inline-flex items-center justify-center font-medium',
  variants: {
    variant: {
      default: 'bg-muted text-muted-foreground',
      success: 'bg-primary/10 text-primary border border-primary/20',
      warning: 'bg-accent/10 text-accent-foreground border border-accent/20',
      error: 'bg-destructive/10 text-destructive border border-destructive/20',
      info: 'bg-secondary/10 text-secondary-foreground border border-secondary/20',
    },
    size: {
      sm: 'px-2 py-0.5 text-xs',
      md: 'px-3 py-1 text-sm',
      lg: 'px-4 py-1.5 text-base',
    },
    shape: {
      rounded: 'rounded-md',
      pill: 'rounded-full',
      square: 'rounded-none',
    },
  },
  defaultVariants: {
    variant: 'default',
    size: 'md',
    shape: 'rounded',
  },
});

export const Badge = ({
  variant,
  size,
  shape,
  className,
  children
}: BadgeProps) => {
  return (
    <Box className={badgeStyles({ variant, size, shape, class: className })}>
      <Text>{children}</Text>
    </Box>
  );
};

// Usage:
// <Badge variant="success" size="lg" shape="pill">Active</Badge>
// <Badge variant="error" size="sm">Error</Badge>
```

**Key Points:**
- ✅ Uses `tva` for variant management
- ✅ Base styles apply to all variants
- ✅ Multiple variant dimensions (variant, size, shape)
- ✅ Default variants specified
- ✅ className override support with `class` parameter
- ✅ TypeScript types for variant options

## Extending Existing Gluestack Components

### Template: Adding Custom Variants to Button

```tsx
import React from 'react';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { Button as GluestackButton, ButtonText } from '@/components/ui/button';

// Define additional variant styles
const customButtonStyles = tva({
  base: '',
  variants: {
    variant: {
      // Extend existing variants with new ones
      gradient: 'bg-gradient-to-r from-primary to-accent',
      glass: 'bg-background/20 backdrop-blur-lg border border-border/50',
      neon: 'bg-transparent border-2 border-primary shadow-[0_0_15px_rgba(59,130,246,0.5)]',
    },
    size: {
      // Add custom sizes
      xs: 'px-2 py-1',
      xl: 'px-8 py-4',
    },
  },
});

interface CustomButtonProps {
  readonly variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'gradient' | 'glass' | 'neon';
  readonly size?: 'default' | 'sm' | 'lg' | 'icon' | 'xs' | 'xl';
  readonly className?: string;
  readonly onPress?: () => void;
  readonly isDisabled?: boolean;
  readonly children: React.ReactNode;
}

export const CustomButton = ({
  variant = 'default',
  size = 'default',
  className,
  onPress,
  isDisabled,
  children,
}: CustomButtonProps) => {
  // Use Gluestack Button for built-in variants
  if (['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'].includes(variant)) {
    return (
      <GluestackButton
        variant={variant as any}
        size={['default', 'sm', 'lg', 'icon'].includes(size) ? size as any : 'default'}
        onPress={onPress}
        isDisabled={isDisabled}
        className={className}
      >
        {children}
      </GluestackButton>
    );
  }

  // Use custom variants
  return (
    <GluestackButton
      onPress={onPress}
      isDisabled={isDisabled}
      className={customButtonStyles({ variant: variant as any, size: size as any, class: className })}
    >
      {children}
    </GluestackButton>
  );
};

// Usage:
// <CustomButton variant="gradient" size="xl">
//   <ButtonText>Gradient Button</ButtonText>
// </CustomButton>
//
// <CustomButton variant="neon" size="lg">
//   <ButtonText>Neon Button</ButtonText>
// </CustomButton>
```

**Key Points:**
- ✅ Extends existing component
- ✅ Preserves original variants
- ✅ Adds new custom variants
- ✅ Maintains compound component pattern
- ✅ Type-safe variant options

## Parent-Child Variant Relationships

When creating components with sub-components, use `parentVariants` to style children based on parent state.

### Template: Card with Variant-Aware Children

```tsx
import React from 'react';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { Box } from '@/components/ui/box';
import { Heading } from '@/components/ui/heading';
import { Text } from '@/components/ui/text';

interface CardProps {
  readonly variant?: 'default' | 'elevated' | 'outlined' | 'ghost';
  readonly colorScheme?: 'neutral' | 'primary' | 'success' | 'error';
  readonly className?: string;
  readonly children: React.ReactNode;
}

interface CardHeaderProps {
  readonly className?: string;
  readonly children: React.ReactNode;
}

interface CardBodyProps {
  readonly className?: string;
  readonly children: React.ReactNode;
}

// Parent card styles
const cardStyles = tva({
  base: 'rounded-lg overflow-hidden',
  variants: {
    variant: {
      default: 'border border-border bg-card',
      elevated: 'shadow-hard-2 bg-card',
      outlined: 'border-2 border-border bg-transparent',
      ghost: 'bg-transparent',
    },
    colorScheme: {
      neutral: '',
      primary: 'border-primary/20',
      success: 'border-primary/20',
      error: 'border-destructive/20',
    },
  },
  compoundVariants: [
    {
      variant: 'default',
      colorScheme: 'primary',
      class: 'bg-primary/5',
    },
    {
      variant: 'default',
      colorScheme: 'success',
      class: 'bg-primary/5',
    },
    {
      variant: 'default',
      colorScheme: 'error',
      class: 'bg-destructive/5',
    },
  ],
  defaultVariants: {
    variant: 'default',
    colorScheme: 'neutral',
  },
});

// Child styles that respond to parent variants
const cardHeaderStyles = tva({
  base: 'p-4 border-b',
  parentVariants: {
    variant: {
      default: 'border-border',
      elevated: 'border-border/50',
      outlined: 'border-border',
      ghost: 'border-transparent',
    },
    colorScheme: {
      neutral: '',
      primary: 'border-primary/20 bg-primary/5',
      success: 'border-primary/20 bg-primary/5',
      error: 'border-destructive/20 bg-destructive/5',
    },
  },
});

const cardBodyStyles = tva({
  base: 'p-4',
  parentVariants: {
    colorScheme: {
      neutral: '',
      primary: '',
      success: '',
      error: '',
    },
  },
});

// Context to share variant state with children
const CardContext = React.createContext<Pick<CardProps, 'variant' | 'colorScheme'>>({
  variant: 'default',
  colorScheme: 'neutral',
});

export const Card = ({
  variant = 'default',
  colorScheme = 'neutral',
  className,
  children
}: CardProps) => {
  return (
    <CardContext.Provider value={{ variant, colorScheme }}>
      <Box className={cardStyles({ variant, colorScheme, class: className })}>
        {children}
      </Box>
    </CardContext.Provider>
  );
};

export const CardHeader = ({ className, children }: CardHeaderProps) => {
  const { variant, colorScheme } = React.useContext(CardContext);
  return (
    <Box className={cardHeaderStyles({ parentVariants: { variant, colorScheme }, class: className })}>
      {children}
    </Box>
  );
};

export const CardBody = ({ className, children }: CardBodyProps) => {
  const { variant, colorScheme } = React.useContext(CardContext);
  return (
    <Box className={cardBodyStyles({ parentVariants: { colorScheme }, class: className })}>
      {children}
    </Box>
  );
};

// Usage:
// <Card variant="elevated" colorScheme="primary">
//   <CardHeader>
//     <Heading size="lg">Success Card</Heading>
//   </CardHeader>
//   <CardBody>
//     <Text>This card responds to parent variants</Text>
//   </CardBody>
// </Card>
```

**Key Points:**
- ✅ Parent context shares variant state
- ✅ Children use `parentVariants` to style based on parent
- ✅ Compound variants for complex combinations
- ✅ Type-safe context usage
- ✅ Flexible composition

## Compound Variants

Use compound variants when combinations of variant options need special styling.

### Template: Button with Compound Variants

```tsx
import React from 'react';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { Button, ButtonText, ButtonIcon } from '@/components/ui/button';
import { Loader2Icon } from '@/components/ui/icon';

interface ActionButtonProps {
  readonly variant?: 'solid' | 'outline' | 'ghost';
  readonly colorScheme?: 'primary' | 'secondary' | 'destructive';
  readonly size?: 'sm' | 'md' | 'lg';
  readonly isLoading?: boolean;
  readonly isDisabled?: boolean;
  readonly className?: string;
  readonly onPress?: () => void;
  readonly children: React.ReactNode;
}

const actionButtonStyles = tva({
  base: 'rounded-md font-medium transition-colors',
  variants: {
    variant: {
      solid: '',
      outline: 'border-2 bg-transparent',
      ghost: 'bg-transparent',
    },
    colorScheme: {
      primary: '',
      secondary: '',
      destructive: '',
    },
    size: {
      sm: 'px-3 py-1.5 text-sm',
      md: 'px-4 py-2 text-base',
      lg: 'px-6 py-3 text-lg',
    },
  },
  compoundVariants: [
    // Solid + Primary
    {
      variant: 'solid',
      colorScheme: 'primary',
      class: 'bg-primary text-primary-foreground data-[hover=true]:bg-primary/90',
    },
    // Solid + Destructive
    {
      variant: 'solid',
      colorScheme: 'destructive',
      class: 'bg-destructive text-white data-[hover=true]:bg-destructive/90',
    },
    // Outline + Primary
    {
      variant: 'outline',
      colorScheme: 'primary',
      class: 'border-primary text-primary data-[hover=true]:bg-primary/10',
    },
    // Outline + Destructive
    {
      variant: 'outline',
      colorScheme: 'destructive',
      class: 'border-destructive text-destructive data-[hover=true]:bg-destructive/10',
    },
    // Ghost + Primary
    {
      variant: 'ghost',
      colorScheme: 'primary',
      class: 'text-primary data-[hover=true]:bg-primary/10',
    },
    // Ghost + Destructive
    {
      variant: 'ghost',
      colorScheme: 'destructive',
      class: 'text-destructive data-[hover=true]:bg-destructive/10',
    },
  ],
  defaultVariants: {
    variant: 'solid',
    colorScheme: 'primary',
    size: 'md',
  },
});

export const ActionButton = ({
  variant,
  colorScheme,
  size,
  isLoading = false,
  isDisabled = false,
  className,
  onPress,
  children,
}: ActionButtonProps) => {
  return (
    <Button
      onPress={onPress}
      isDisabled={isDisabled || isLoading}
      className={actionButtonStyles({ variant, colorScheme, size, class: className })}
    >
      {isLoading && <ButtonIcon as={Loader2Icon} className="animate-spin" />}
      {children}
    </Button>
  );
};

// Usage:
// <ActionButton variant="solid" colorScheme="primary" size="lg">
//   <ButtonText>Primary Action</ButtonText>
// </ActionButton>
//
// <ActionButton variant="outline" colorScheme="destructive" isLoading>
//   <ButtonText>Delete</ButtonText>
// </ActionButton>
```

**Key Points:**
- ✅ Compound variants handle specific combinations
- ✅ Base variants provide defaults
- ✅ Hover states with data attributes
- ✅ Loading state integration
- ✅ Flexible variant combinations

## Common Variant Patterns

### Pattern 1: Status Badges

```tsx
const statusBadgeStyles = tva({
  base: 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
  variants: {
    status: {
      active: 'bg-primary/10 text-primary',
      inactive: 'bg-muted text-muted-foreground',
      pending: 'bg-accent/10 text-accent-foreground',
      completed: 'bg-primary/10 text-primary',
      failed: 'bg-destructive/10 text-destructive',
    },
  },
  defaultVariants: {
    status: 'inactive',
  },
});

// Usage:
// <Box className={statusBadgeStyles({ status: 'active' })}>
//   <Text>Active</Text>
// </Box>
```

### Pattern 2: Alert Variants

```tsx
const alertStyles = tva({
  base: 'rounded-lg border p-4',
  variants: {
    severity: {
      info: 'bg-secondary/10 border-secondary/20 text-secondary-foreground',
      success: 'bg-primary/10 border-primary/20 text-primary',
      warning: 'bg-accent/10 border-accent/20 text-accent-foreground',
      error: 'bg-destructive/10 border-destructive/20 text-destructive',
    },
  },
  defaultVariants: {
    severity: 'info',
  },
});

// Usage:
// <Box className={alertStyles({ severity: 'error' })}>
//   <Text>Error message</Text>
// </Box>
```

### Pattern 3: Interactive Card States

```tsx
const interactiveCardStyles = tva({
  base: 'rounded-lg border border-border p-4 transition-all cursor-pointer',
  variants: {
    state: {
      default: 'bg-card data-[hover=true]:bg-muted/50',
      selected: 'bg-primary/10 border-primary',
      disabled: 'bg-muted opacity-60 cursor-not-allowed',
    },
  },
  defaultVariants: {
    state: 'default',
  },
});

// Usage:
// <Pressable>
//   <Box className={interactiveCardStyles({ state: 'selected' })}>
//     <Text>Selected Card</Text>
//   </Box>
// </Pressable>
```

### Pattern 4: Size Variants with Consistent Ratios

```tsx
const avatarStyles = tva({
  base: 'rounded-full overflow-hidden',
  variants: {
    size: {
      xs: 'w-6 h-6',
      sm: 'w-8 h-8',
      md: 'w-12 h-12',
      lg: 'w-16 h-16',
      xl: 'w-20 h-20',
      '2xl': 'w-24 h-24',
    },
  },
  defaultVariants: {
    size: 'md',
  },
});

// Usage:
// <Image className={avatarStyles({ size: 'lg' })} source={{ uri: avatarUrl }} />
```

## Best Practices for Variants

### ✅ Do's

1. **Use semantic variant names**
   ```tsx
   // ✅ GOOD: Semantic names
   variant: 'primary' | 'secondary' | 'destructive'

   // ❌ BAD: Generic names
   variant: 'blue' | 'red' | 'green'
   ```

2. **Provide default variants**
   ```tsx
   // ✅ GOOD: Always specify defaults
   defaultVariants: {
     variant: 'default',
     size: 'md',
   }
   ```

3. **Use compound variants for combinations**
   ```tsx
   // ✅ GOOD: Handle specific combinations
   compoundVariants: [
     {
       variant: 'outline',
       colorScheme: 'primary',
       class: 'border-primary text-primary',
     },
   ]
   ```

4. **Keep variant dimensions focused**
   ```tsx
   // ✅ GOOD: Clear separation
   variants: {
     variant: { ... },  // Visual style
     size: { ... },     // Size
     state: { ... },    // Interactive state
   }
   ```

5. **Use ONLY semantic tokens in variant styles - NO EXCEPTIONS**
   ```tsx
   // ✅ CORRECT: Semantic tokens with alpha values
   success: 'bg-primary/10 text-primary border-primary/20'
   error: 'bg-destructive/10 text-destructive border-destructive/20'
   muted: 'bg-muted text-muted-foreground border-border'

   // ❌ PROHIBITED: Numbered color tokens
   success: 'bg-green-100 text-green-800 border-green-200'
   error: 'bg-red-100 text-red-800 border-red-200'

   // ❌ PROHIBITED: Generic tokens
   muted: 'bg-neutral-100 text-neutral-600 border-neutral-300'
   muted: 'bg-gray-100 text-gray-600 border-gray-300'

   // ❌ PROHIBITED: Typography tokens
   text: 'text-typography-900'
   ```

### ❌ Don'ts

1. **Don't create too many variant dimensions**
   ```tsx
   // ❌ BAD: Too many dimensions
   variants: {
     variant: { ... },
     size: { ... },
     color: { ... },
     border: { ... },
     shadow: { ... },
     rounded: { ... },
   }

   // ✅ GOOD: Focused dimensions
   variants: {
     variant: { ... },
     size: { ... },
   }
   ```

2. **Don't mix concerns in variant names**
   ```tsx
   // ❌ BAD: Mixing visual and semantic
   variant: 'primary' | 'large-primary' | 'small-secondary'

   // ✅ GOOD: Separate dimensions
   variant: 'primary' | 'secondary'
   size: 'sm' | 'md' | 'lg'
   ```

3. **Don't duplicate existing component props**
   ```tsx
   // ❌ BAD: Duplicating Button's variant prop
   const CustomButton = ({ variant, ... }: { variant: 'new1' | 'new2' })

   // ✅ GOOD: Extend existing variants
   const CustomButton = ({ variant, ... }: {
     variant: 'default' | 'outline' | 'new1' | 'new2'
   })
   ```

## CRITICAL: Semantic Tokens in Variants

**ALL variant styles MUST use ONLY semantic tokens. This is NON-NEGOTIABLE.**

### Correct Variant Token Usage

```tsx
// ✅ CORRECT: All colors are semantic tokens
const badgeStyles = tva({
  base: 'inline-flex items-center rounded-full px-3 py-1',
  variants: {
    variant: {
      default: 'bg-muted text-muted-foreground',
      primary: 'bg-primary/10 text-primary border border-primary/20',
      success: 'bg-primary/10 text-primary border border-primary/20',
      error: 'bg-destructive/10 text-destructive border border-destructive/20',
      warning: 'bg-accent/10 text-accent-foreground border border-accent/20',
    },
  },
});
```

### Prohibited Variant Token Usage

```tsx
// ❌ PROHIBITED: Using numbered color tokens
const badgeStyles = tva({
  variants: {
    variant: {
      success: 'bg-green-100 text-green-800 border-green-200', // ❌ NO
      error: 'bg-red-100 text-red-800 border-red-200',         // ❌ NO
      warning: 'bg-yellow-100 text-yellow-800',                // ❌ NO
    },
  },
});

// ❌ PROHIBITED: Using generic tokens
const badgeStyles = tva({
  variants: {
    variant: {
      default: 'bg-neutral-100 text-neutral-700',              // ❌ NO
      muted: 'bg-gray-100 text-gray-600',                      // ❌ NO
    },
  },
});

// ❌ PROHIBITED: Using typography tokens
const textStyles = tva({
  variants: {
    variant: {
      heading: 'text-typography-900',                          // ❌ NO
      body: 'text-typography-700',                             // ❌ NO
    },
  },
});
```

### Token Replacement Guide for Variants

| Prohibited Pattern | Use Instead |
| ------------------ | ----------- |
| `bg-green-100 text-green-800` | `bg-primary/10 text-primary` |
| `bg-red-100 text-red-800` | `bg-destructive/10 text-destructive` |
| `bg-yellow-100 text-yellow-800` | `bg-accent/10 text-accent-foreground` |
| `bg-blue-100 text-blue-800` | `bg-primary/10 text-primary` |
| `bg-neutral-100 text-neutral-700` | `bg-muted text-muted-foreground` |
| `bg-gray-100 text-gray-900` | `bg-muted text-foreground` |
| `text-typography-900` | `text-foreground` |
| `text-typography-600` | `text-muted-foreground` |
| `border-gray-300` | `border-border` |

## Validation Checklist for Variants

When creating variants, verify:

- [ ] **CRITICAL: NO prohibited tokens** - No `typography-*`, `neutral-*`, `gray-*`, `slate-*`, numbered colors
- [ ] **All colors are semantic tokens** - Every color uses semantic tokens from the approved list
- [ ] **Alpha values instead of opacity** - Uses `/70`, `/90` instead of `opacity-*` utilities
- [ ] Variant names are semantic (not color names)
- [ ] Default variants specified
- [ ] Spacing uses scale values (no arbitrary values)
- [ ] TypeScript types match variant options
- [ ] className override supported with `class` parameter
- [ ] Parent variants used for child components (if applicable)
- [ ] Compound variants for complex combinations (if needed)
- [ ] Data attributes for interactive states
- [ ] Tested with dark mode (semantic tokens ensure compatibility)
- [ ] Documentation/comments for non-obvious variants

## Recipe: Converting Repeated Styles to Variants

### Before: Repeated className Patterns

```tsx
// ❌ Repeated patterns across codebase
<Box className="bg-primary/10 border border-primary/20 rounded-full px-3 py-1">
  <Text className="text-xs text-primary font-semibold">Active</Text>
</Box>

<Box className="bg-destructive/10 border border-destructive/20 rounded-full px-3 py-1">
  <Text className="text-xs text-destructive font-semibold">Error</Text>
</Box>

<Box className="bg-accent/10 border border-accent/20 rounded-full px-3 py-1">
  <Text className="text-xs text-accent-foreground font-semibold">Pending</Text>
</Box>
```

### After: Variant-Based Component

```tsx
// ✅ GOOD: Single component with variants
const StatusPill = ({ status, children }: StatusPillProps) => {
  const pillStyles = tva({
    base: 'inline-flex items-center rounded-full px-3 py-1',
    variants: {
      status: {
        active: 'bg-primary/10 border border-primary/20',
        error: 'bg-destructive/10 border border-destructive/20',
        pending: 'bg-accent/10 border border-accent/20',
      },
    },
  });

  const textStyles = tva({
    base: 'text-xs font-semibold',
    parentVariants: {
      status: {
        active: 'text-primary',
        error: 'text-destructive',
        pending: 'text-accent-foreground',
      },
    },
  });

  return (
    <Box className={pillStyles({ status })}>
      <Text className={textStyles({ parentVariants: { status } })}>{children}</Text>
    </Box>
  );
};

// Usage:
<StatusPill status="active">Active</StatusPill>
<StatusPill status="error">Error</StatusPill>
<StatusPill status="pending">Pending</StatusPill>
```

## Troubleshooting

### Issue: Variants Not Applying

**Problem**: Variant classes not showing up

**Solution**:
1. Check Tailwind config includes tva patterns
2. Verify className merge order
3. Ensure no conflicting inline styles

### Issue: Parent Variants Not Working

**Problem**: Child components don't respond to parent variants

**Solution**:
1. Use context to share parent state
2. Pass parentVariants object correctly
3. Verify context provider wraps children

### Issue: Type Errors with Variants

**Problem**: TypeScript errors with variant options

**Solution**:
1. Define variant types in interface
2. Use literal types for variant values
3. Ensure defaultVariants match types

## Reference

- **tva Documentation**: https://www.tailwind-variants.org/
- **Gluestack v4 Docs**: https://v4.gluestack.io/ui/docs
- **Component Examples**: `https://v4.gluestack.io/ui/docs/components/${componentName}/`