| 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:
- Repeating the same style combination - Multiple places use the same className pattern
- Project-specific design patterns - Brand-specific button styles, card types, etc.
- Conditional styling - Component appearance changes based on props
- Extending existing components - Adding new visual styles to Gluestack components
- 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:
- What's the base component? - Button, Card, Badge, etc.
- What visual states are needed? - Colors, sizes, borders, shadows
- Are there sub-components? - ButtonText, CardHeader, etc.
- What props should control variants? - variant, size, state props
- Should variants affect children? - Parent variants for sub-components
Step 2: Plan Variant Structure
Define your variant system:
{
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
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;
}
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',
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
},
: {
: ,
: ,
: ,
},
});
= () => {
(
);
};
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
import React from 'react';
import { tva } from '@gluestack-ui/utils/nativewind-utils';
import { Button as GluestackButton, ButtonText } from '@/components/ui/button';
const customButtonStyles = tva({
base: '',
variants: {
variant: {
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: {
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?: | | | | | ;
?: ;
?: ;
?: ;
: .;
}
= () => {
([, , , , , ].(variant)) {
(
);
}
(
);
};
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
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?: ;
: .;
}
cardStyles = ({
: ,
: {
: {
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
},
},
: [
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
],
: {
: ,
: ,
},
});
cardHeaderStyles = ({
: ,
: {
: {
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
},
},
});
cardBodyStyles = ({
: ,
: {
: {
: ,
: ,
: ,
: ,
},
},
});
= .<<, | >>({
: ,
: ,
});
= () => {
(
);
};
= () => {
{ variant, colorScheme } = .();
(
);
};
= () => {
{ variant, colorScheme } = .();
(
);
};
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
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({
: ,
: {
: {
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
},
: [
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
{
: ,
: ,
: ,
},
],
: {
: ,
: ,
: ,
},
});
= () => {
(
);
};
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
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',
},
});
Pattern 2: Alert Variants
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',
},
});
Pattern 3: Interactive Card States
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',
},
});
Pattern 4: Size Variants with Consistent Ratios
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',
},
});
Best Practices for Variants
✅ Do's
-
Use semantic variant names
variant: 'primary' | 'secondary' | 'destructive'
variant: 'blue' | 'red' | 'green'
-
Provide default variants
defaultVariants: {
variant: 'default',
size: 'md',
}
-
Use compound variants for combinations
compoundVariants: [
{
variant: 'outline',
colorScheme: 'primary',
class: 'border-primary text-primary',
},
]
-
Keep variant dimensions focused
variants: {
variant: { ... },
size: { ... },
state: { ... },
}
-
Use ONLY semantic tokens in variant styles - NO EXCEPTIONS
success: 'bg-primary/10 text-primary border-primary/20'
error: 'bg-destructive/10 text-destructive border-destructive/20'
:
:
:
:
:
:
❌ Don'ts
-
Don't create too many variant dimensions
variants: {
variant: { ... },
size: { ... },
color: { ... },
border: { ... },
shadow: { ... },
rounded: { ... },
}
variants: {
variant: { ... },
size: { ... },
}
-
Don't mix concerns in variant names
variant: 'primary' | 'large-primary' | 'small-secondary'
variant: 'primary' | 'secondary'
size: 'sm' | 'md' | 'lg'
-
Don't duplicate existing component props
const CustomButton = ({ variant, ... }: { variant: 'new1' | 'new2' })
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
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
const badgeStyles = tva({
variants: {
variant: {
success: 'bg-green-100 text-green-800 border-green-200',
error: 'bg-red-100 text-red-800 border-red-200',
warning: 'bg-yellow-100 text-yellow-800',
},
},
});
const badgeStyles = tva({
variants: {
variant: {
default: 'bg-neutral-100 text-neutral-700',
muted: 'bg-gray-100 text-gray-600',
},
},
});
const textStyles = tva({
variants: {
variant: {
heading: 'text-typography-900',
body: 'text-typography-700',
},
},
});
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:
Recipe: Converting Repeated Styles to Variants
Before: Repeated className Patterns
<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
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>
);
};
<StatusPill status=>Active
Troubleshooting
Issue: Variants Not Applying
Problem: Variant classes not showing up
Solution:
- Check Tailwind config includes tva patterns
- Verify className merge order
- Ensure no conflicting inline styles
Issue: Parent Variants Not Working
Problem: Child components don't respond to parent variants
Solution:
- Use context to share parent state
- Pass parentVariants object correctly
- Verify context provider wraps children
Issue: Type Errors with Variants
Problem: TypeScript errors with variant options
Solution:
- Define variant types in interface
- Use literal types for variant values
- Ensure defaultVariants match types
Reference