Gluestack UI v4 - Creating Components
This sub-skill provides practical guidance for creating new components using gluestack-ui v4, from planning to implementation.
Component Creation Workflow
Step 1: Plan Component Structure
Before writing code, answer these questions:
- •
What is the component's purpose?
- •Form input, data display, navigation, layout, etc.
- •
Which Gluestack components do I need?
- •Check official docs:
https://v4.gluestack.io/ui/docs/components/${componentName}/ - •Use Gluestack wrappers, not React Native primitives
- •Check official docs:
- •
Does it need compound components?
- •Multiple related sub-components (Header, Body, Footer)
- •Icon + Text combinations
- •Label + Input patterns
- •
What props should it accept?
- •Size variants:
sm,md,lg - •Visual variants:
default,outline,ghost - •State props:
isDisabled,isInvalid,isLoading - •Custom className for overrides
- •Size variants:
- •
Does it need variants?
- •If yes, use
tva(Tailwind Variant Authority) - •Define base styles and variant options
- •If yes, use
Step 2: Check Official Documentation
ALWAYS verify component usage before creating:
# Visit official docs for the component
https://v4.gluestack.io/ui/docs/components/${componentName}/
Check for:
- •Latest API and props
- •Required sub-components
- •Usage examples
- •Accessibility features
Step 3: Create Component File
Follow this file structure:
components/
├── ui/ # Gluestack UI components (copy-paste)
│ ├── box/
│ ├── button/
│ └── input/
└── custom/ # Your custom components
├── profile-card/
│ └── index.tsx
└── login-form/
└── index.tsx
Component Templates
Template 1: Simple Component (No Variants)
Use when component has consistent styling without variants.
import React from 'react';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Heading } from '@/components/ui/heading';
interface ProfileCardProps {
readonly name: string;
readonly email: string;
readonly className?: string;
}
export const ProfileCard = ({ name, email, className }: ProfileCardProps) => {
return (
<Box className={`bg-card rounded-lg border border-border p-4 ${className || ''}`}>
<Heading size="lg" className="text-card-foreground">
{name}
</Heading>
<Text size="sm" className="text-muted-foreground mt-1">
{email}
</Text>
</Box>
);
};
Key points:
- •✅ Uses Gluestack components (Box, Text, Heading)
- •✅ TypeScript interface with
readonlyprops - •✅ Semantic tokens (bg-card, text-card-foreground)
- •✅ Accepts className for customization
- •✅ Component props for sizing (Heading size)
Template 2: Component with Variants (Using tva)
Use when component needs multiple visual styles or sizes.
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 AlertProps {
readonly variant?: 'default' | 'success' | 'warning' | 'destructive';
readonly size?: 'sm' | 'md' | 'lg';
readonly className?: string;
readonly children: React.ReactNode;
}
const alertStyles = tva({
base: 'rounded-lg border p-4',
variants: {
variant: {
default: 'bg-card border-border',
success: 'bg-primary/10 border-primary',
warning: 'bg-accent/10 border-accent',
destructive: 'bg-destructive/10 border-destructive',
},
size: {
sm: 'p-2',
md: 'p-4',
lg: 'p-6',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
});
const alertTextStyles = tva({
base: 'font-sans',
parentVariants: {
variant: {
default: 'text-foreground',
success: 'text-primary',
warning: 'text-accent-foreground',
destructive: 'text-destructive',
},
size: {
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base',
},
},
});
export const Alert = ({ variant, size, className, children }: AlertProps) => {
return (
<Box className={alertStyles({ variant, size, class: className })}>
<Text className={alertTextStyles({ parentVariants: { variant, size } })}>
{children}
</Text>
</Box>
);
};
Key points:
- •✅ Uses tva for variant management
- •✅ Base styles + variant options
- •✅ Default variants specified
- •✅ Parent variants for child components
- •✅ className override support
Template 3: Compound Component Pattern
Use when component has multiple related sub-components.
import React from 'react';
import { Box } from '@/components/ui/box';
import { Heading } from '@/components/ui/heading';
import { Text } from '@/components/ui/text';
import { HStack } from '@/components/ui/hstack';
// Main Card Component
interface CardProps {
readonly className?: string;
readonly children: React.ReactNode;
}
export const Card = ({ className, children }: CardProps) => {
return (
<Box className={`bg-card rounded-lg border border-border shadow-sm ${className || ''}`}>
{children}
</Box>
);
};
// Card Header Sub-component
interface CardHeaderProps {
readonly className?: string;
readonly children: React.ReactNode;
}
export const CardHeader = ({ className, children }: CardHeaderProps) => {
return (
<Box className={`p-4 border-b border-border ${className || ''}`}>
{children}
</Box>
);
};
// Card Body Sub-component
interface CardBodyProps {
readonly className?: string;
readonly children: React.ReactNode;
}
export const CardBody = ({ className, children }: CardBodyProps) => {
return (
<Box className={`p-4 ${className || ''}`}>
{children}
</Box>
);
};
// Card Footer Sub-component
interface CardFooterProps {
readonly className?: string;
readonly children: React.ReactNode;
}
export const CardFooter = ({ className, children }: CardFooterProps) => {
return (
<HStack space="md" className={`p-4 border-t border-border ${className || ''}`}>
{children}
</HStack>
);
};
// Usage
// <Card>
// <CardHeader>
// <Heading size="lg">Title</Heading>
// </CardHeader>
// <CardBody>
// <Text>Content</Text>
// </CardBody>
// <CardFooter>
// <Button>Action</Button>
// </CardFooter>
// </Card>
Key points:
- •✅ Main component + sub-components
- •✅ Each sub-component is independent
- •✅ Consistent styling across sub-components
- •✅ Flexible composition
Template 4: Form Component
Use for form inputs with labels, validation, and error messages.
import React, { useState } from 'react';
import { FormControl, FormControlLabel, FormControlLabelText } from '@/components/ui/form-control';
import { FormControlError, FormControlErrorIcon, FormControlErrorText } from '@/components/ui/form-control';
import { FormControlHelper, FormControlHelperText } from '@/components/ui/form-control';
import { Input, InputField, InputSlot, InputIcon } from '@/components/ui/input';
import { MailIcon, AlertCircleIcon } from '@/components/ui/icon';
interface EmailInputProps {
readonly label?: string;
readonly placeholder?: string;
readonly helperText?: string;
readonly value: string;
readonly error?: string;
readonly onChange: (value: string) => void;
readonly className?: string;
}
export const EmailInput = ({
label = 'Email Address',
placeholder = 'Enter your email',
helperText,
value,
error,
onChange,
className,
}: EmailInputProps) => {
const [isFocused, setIsFocused] = useState(false);
return (
<FormControl isInvalid={!!error} className={className}>
<FormControlLabel>
<FormControlLabelText>{label}</FormControlLabelText>
</FormControlLabel>
<Input>
<InputSlot>
<InputIcon
as={MailIcon}
className={isFocused ? 'text-primary' : 'text-muted-foreground'}
/>
</InputSlot>
<InputField
placeholder={placeholder}
value={value}
onChangeText={onChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
keyboardType="email-address"
autoCapitalize="none"
/>
</Input>
{error && (
<FormControlError>
<FormControlErrorIcon as={AlertCircleIcon} />
<FormControlErrorText>{error}</FormControlErrorText>
</FormControlError>
)}
{helperText && !error && (
<FormControlHelper>
<FormControlHelperText>{helperText}</FormControlHelperText>
</FormControlHelper>
)}
</FormControl>
);
};
Key points:
- •✅ FormControl wrapper for validation
- •✅ InputIcon wrapped in InputSlot (CRITICAL)
- •✅ Error and helper text handling
- •✅ Focus state management
- •✅ Proper keyboard type
Template 5: Interactive Component with State
Use for components with internal state and interactions.
import React, { useState } from 'react';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { Pressable } from '@/components/ui/pressable';
import { ChevronDownIcon, ChevronUpIcon } from '@/components/ui/icon';
import { Icon } from '@/components/ui/icon';
import { VStack } from '@/components/ui/vstack';
import { HStack } from '@/components/ui/hstack';
interface AccordionProps {
readonly title: string;
readonly children: React.ReactNode;
readonly defaultExpanded?: boolean;
readonly className?: string;
}
export const Accordion = ({
title,
children,
defaultExpanded = false,
className,
}: AccordionProps) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
<Box className={`border border-border rounded-lg overflow-hidden ${className || ''}`}>
<Pressable onPress={() => setIsExpanded(!isExpanded)}>
<HStack
space="md"
className="p-4 bg-card items-center justify-between"
>
<Text size="md" bold className="text-card-foreground flex-1">
{title}
</Text>
<Icon
as={isExpanded ? ChevronUpIcon : ChevronDownIcon}
size="md"
className="text-muted-foreground"
/>
</HStack>
</Pressable>
{isExpanded && (
<Box className="p-4 bg-background border-t border-border">
{children}
</Box>
)}
</Box>
);
};
Key points:
- •✅ Internal state management
- •✅ Conditional rendering
- •✅ Interactive elements (Pressable)
- •✅ Icon state changes
- •✅ Layout with space prop
Template 6: Component with Loading State
Use for components that fetch data or perform async operations.
import React from 'react';
import { Button, ButtonText, ButtonSpinner, ButtonIcon } from '@/components/ui/button';
import { CheckIcon } from '@/components/ui/icon';
interface SubmitButtonProps {
readonly isLoading?: boolean;
readonly isSuccess?: boolean;
readonly onPress: () => void;
readonly className?: string;
}
export const SubmitButton = ({
isLoading = false,
isSuccess = false,
onPress,
className,
}: SubmitButtonProps) => {
return (
<Button
variant="default"
size="lg"
onPress={onPress}
isDisabled={isLoading || isSuccess}
className={className}
>
{isLoading && <ButtonSpinner />}
{isSuccess && <ButtonIcon as={CheckIcon} />}
<ButtonText>
{isLoading ? 'Submitting...' : isSuccess ? 'Success!' : 'Submit'}
</ButtonText>
</Button>
);
};
Key points:
- •✅ Loading state with ButtonSpinner
- •✅ Success state with icon
- •✅ Disabled during loading
- •✅ Dynamic text based on state
Common Component Recipes
Recipe 1: Profile Card with Avatar
import React from 'react';
import { Box } from '@/components/ui/box';
import { HStack } from '@/components/ui/hstack';
import { VStack } from '@/components/ui/vstack';
import { Text } from '@/components/ui/text';
import { Heading } from '@/components/ui/heading';
import { Image } from '@/components/ui/image';
interface ProfileCardProps {
readonly name: string;
readonly email: string;
readonly avatarUrl: string;
readonly bio?: string;
readonly className?: string;
}
export const ProfileCard = ({
name,
email,
avatarUrl,
bio,
className,
}: ProfileCardProps) => {
return (
<Box className={`bg-card rounded-lg border border-border p-4 ${className || ''}`}>
<HStack space="lg" className="items-start">
<Image
source={{ uri: avatarUrl }}
alt={`${name}'s avatar`}
className="w-16 h-16 rounded-full"
/>
<VStack space="sm" className="flex-1">
<Heading size="lg" className="text-card-foreground">
{name}
</Heading>
<Text size="sm" className="text-muted-foreground">
{email}
</Text>
{bio && (
<Text size="sm" className="text-foreground mt-2">
{bio}
</Text>
)}
</VStack>
</HStack>
</Box>
);
};
Recipe 2: Status Badge
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 StatusBadgeProps {
readonly status: 'active' | 'inactive' | 'pending' | 'error';
readonly className?: string;
}
const badgeStyles = tva({
base: 'rounded-full px-3 py-1 inline-flex',
variants: {
status: {
active: 'bg-primary/10',
inactive: 'bg-muted',
pending: 'bg-accent/10',
error: 'bg-destructive/10',
},
},
});
const badgeTextStyles = tva({
base: 'text-xs font-medium',
parentVariants: {
status: {
active: 'text-primary',
inactive: 'text-muted-foreground',
pending: 'text-accent-foreground',
error: 'text-destructive',
},
},
});
export const StatusBadge = ({ status, className }: StatusBadgeProps) => {
const labels = {
active: 'Active',
inactive: 'Inactive',
pending: 'Pending',
error: 'Error',
};
return (
<Box className={badgeStyles({ status, class: className })}>
<Text className={badgeTextStyles({ parentVariants: { status } })}>
{labels[status]}
</Text>
</Box>
);
};
Recipe 3: Search Input
import React from 'react';
import { Input, InputField, InputSlot, InputIcon } from '@/components/ui/input';
import { SearchIcon, XIcon } from '@/components/ui/icon';
import { Pressable } from '@/components/ui/pressable';
interface SearchInputProps {
readonly value: string;
readonly onChange: (value: string) => void;
readonly placeholder?: string;
readonly onClear?: () => void;
readonly className?: string;
}
export const SearchInput = ({
value,
onChange,
placeholder = 'Search...',
onClear,
className,
}: SearchInputProps) => {
const handleClear = () => {
onChange('');
onClear?.();
};
return (
<Input className={className}>
<InputSlot>
<InputIcon as={SearchIcon} className="text-muted-foreground" />
</InputSlot>
<InputField
placeholder={placeholder}
value={value}
onChangeText={onChange}
autoCapitalize="none"
/>
{value.length > 0 && (
<InputSlot onPress={handleClear}>
<InputIcon as={XIcon} className="text-muted-foreground" />
</InputSlot>
)}
</Input>
);
};
Recipe 4: List Item with Action
import React from 'react';
import { Pressable } from '@/components/ui/pressable';
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 { Icon } from '@/components/ui/icon';
import { ChevronRightIcon } from '@/components/ui/icon';
interface ListItemProps {
readonly title: string;
readonly description?: string;
readonly onPress: () => void;
readonly showChevron?: boolean;
readonly className?: string;
}
export const ListItem = ({
title,
description,
onPress,
showChevron = true,
className,
}: ListItemProps) => {
return (
<Pressable onPress={onPress}>
<Box
className={`
p-4 border-b border-border bg-background
data-[hover=true]:bg-muted/50
${className || ''}
`}
>
<HStack space="md" className="items-center justify-between">
<VStack space="xs" className="flex-1">
<Text size="md" className="text-foreground">
{title}
</Text>
{description && (
<Text size="sm" className="text-muted-foreground">
{description}
</Text>
)}
</VStack>
{showChevron && (
<Icon
as={ChevronRightIcon}
size="md"
className="text-muted-foreground"
/>
)}
</HStack>
</Box>
</Pressable>
);
};
Best Practices Checklist
When creating a component, verify:
Structure
- • Uses Gluestack components (not React Native primitives)
- • Imports from
@/components/ui/* - • Follows compound component pattern when needed
- • InputIcon wrapped in InputSlot (if using Input)
TypeScript
- • Interface defined with
readonlyprops - • All props typed correctly
- • Optional props have
?marker - • Default values specified in function params
Styling
- • Uses semantic tokens (bg-card, text-foreground, etc.)
- • Spacing uses scale values (p-4, m-2, etc.)
- • Component props used (space, size, variant)
- • className prop for customization
- • Uses tva for variants (if needed)
Props
- • Accepts className for overrides
- • Size variants:
sm,md,lg - • Visual variants if applicable
- • State props:
isDisabled,isLoading,isInvalid - • Callback props for interactions
Accessibility
- • Meaningful alt text for images
- • Proper keyboard types for inputs
- • Focus states handled
- • ARIA labels when needed
Performance
- • Memoized with React.memo if expensive
- • Callbacks wrapped in useCallback
- • No unnecessary rerenders
Common Mistakes to Avoid
❌ Don't: Mix React Native and Gluestack
// ❌ INCORRECT
import { View, Text } from 'react-native';
import { Button } from '@/components/ui/button';
export const Component = () => (
<View>
<Text>Content</Text>
</View>
);
❌ Don't: Skip Sub-Components
// ❌ INCORRECT: Missing ButtonText
<Button onPress={handlePress}>Submit</Button>
// ✅ CORRECT
<Button onPress={handlePress}>
<ButtonText>Submit</ButtonText>
</Button>
❌ Don't: Use Inline Styles
// ❌ INCORRECT
<Box style={{ padding: 16, backgroundColor: '#fff' }}>
// ✅ CORRECT
<Box className="p-4 bg-background">
❌ Don't: Forget InputSlot
// ❌ INCORRECT: InputIcon not wrapped
<Input>
<InputIcon as={MailIcon} />
<InputField />
</Input>
// ✅ CORRECT
<Input>
<InputSlot>
<InputIcon as={MailIcon} />
</InputSlot>
<InputField />
</Input>
Quick Start Guide
- •Choose a template from above that matches your needs
- •Check official docs for the components you'll use
- •Copy the template and modify for your use case
- •Add TypeScript types with readonly props
- •Use semantic tokens for all colors
- •Test on real device to verify behavior
- •Validate with checklist before committing
Reference
- •Component Documentation:
https://v4.gluestack.io/ui/docs/components/${componentName}/ - •Complete Docs: https://v4.gluestack.io/ui/docs
- •tva Documentation: https://www.tailwind-variants.org/