home / skills / gluestack / agent-skills / gluestack-ui-v4
This skill helps you create and manage custom gluestack-ui v4 variants using tva for type-safe, scalable design system extensions.
npx playbooks add skill gluestack/agent-skills --skill gluestack-ui-v4Review the files below or copy the command above to add this skill to your agents.
---
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}/`
This skill guides you through creating custom variants for gluestack-ui v4 components using tva. It explains when to add variants, how to extend existing components, and patterns for parent-child and compound variants. The goal is consistent, type-safe, and composable styling that integrates with Gluestack components.
You define variant dimensions with tva (base, variants, compoundVariants, defaultVariants, parentVariants) and expose typed props on components. For extensions, preserve built-in variants by delegating standard cases to the original component and apply custom tva classes for new ones. Use React context to share parent variant state so children can react via parentVariants.
When should I use compoundVariants?
Use compoundVariants for styling rules that only apply to specific combinations of variant options, avoiding duplicated classes and keeping intent explicit.
How do I keep built-in Gluestack behavior when adding variants?
Delegate built-in variant values to the original component and apply your custom tva output only for new variant keys; this preserves existing behavior and accessibility.