Skip to content

Commit f01d188

Browse files
committed
docs: add component variants guide to advanced section
1 parent 0327860 commit f01d188

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed

docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default defineConfig({
6565
{
6666
label: "Advanced",
6767
items: [
68+
{ label: "Component Variants", slug: "advanced/component-variants" },
6869
{ label: "Custom Attributes", slug: "advanced/custom-attributes" },
6970
{ label: "Custom Styles Identifier", slug: "advanced/custom-styles-identifier" },
7071
{ label: "Custom Colors", slug: "advanced/custom-colors" },
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
---
2+
title: Component Variants
3+
description: Build type-safe components with variant-based styling using tw
4+
---
5+
6+
When building design systems, components often need multiple variants (e.g., primary/secondary buttons, small/medium/large sizes). The `tw` template tag combined with TypeScript provides a powerful pattern for creating type-safe, variant-based components.
7+
8+
## Basic Variant Pattern
9+
10+
Define variants as a record mapping variant names to `TwStyle` objects:
11+
12+
```tsx
13+
import { View, Text, type ViewStyle, type TextStyle } from "react-native";
14+
import { Pressable, tw, type TwStyle } from "@mgcrea/react-native-tailwind";
15+
16+
// Define variant types
17+
type ButtonVariant = "solid" | "outline" | "ghost";
18+
19+
// Define styles for each variant
20+
const variantStyles = {
21+
solid: {
22+
container: tw`bg-blue-500 active:bg-blue-700`,
23+
text: tw`text-white`,
24+
},
25+
outline: {
26+
container: tw`border border-blue-500 bg-white active:bg-blue-50`,
27+
text: tw`text-blue-500`,
28+
},
29+
ghost: {
30+
container: tw`active:bg-blue-50`,
31+
text: tw`text-blue-500`,
32+
},
33+
} satisfies Record<ButtonVariant, { container: TwStyle<ViewStyle>; text: TwStyle<TextStyle> }>;
34+
35+
type ButtonProps = {
36+
title: string;
37+
variant?: ButtonVariant;
38+
onPress?: () => void;
39+
};
40+
41+
export function Button({ title, variant = "solid", onPress }: ButtonProps) {
42+
const styles = variantStyles[variant];
43+
44+
return (
45+
<Pressable
46+
className="px-6 py-3 rounded-lg items-center justify-center"
47+
style={(state) => [styles.container.style, state.pressed && styles.container.activeStyle]}
48+
onPress={onPress}
49+
>
50+
<Text style={styles.text.style}>{title}</Text>
51+
</Pressable>
52+
);
53+
}
54+
```
55+
56+
## Multi-Dimensional Variants
57+
58+
Components often have multiple variant dimensions (e.g., size AND color). Structure your variants accordingly:
59+
60+
```tsx
61+
import { View, Text, type ViewStyle, type TextStyle } from "react-native";
62+
import { Pressable, tw, type TwStyle } from "@mgcrea/react-native-tailwind";
63+
64+
// Size variants
65+
type ButtonSize = "sm" | "md" | "lg";
66+
67+
const sizeVariants = {
68+
sm: {
69+
container: tw`h-9 px-3 rounded-lg`,
70+
text: tw`text-sm`,
71+
},
72+
md: {
73+
container: tw`h-11 px-5 rounded-xl`,
74+
text: tw`text-base`,
75+
},
76+
lg: {
77+
container: tw`h-14 px-8 rounded-2xl`,
78+
text: tw`text-lg`,
79+
},
80+
} satisfies Record<ButtonSize, { container: TwStyle<ViewStyle>; text: TwStyle<TextStyle> }>;
81+
82+
// Color variants
83+
type ButtonColor = "primary" | "secondary" | "destructive";
84+
85+
const colorVariants = {
86+
primary: {
87+
container: tw`bg-blue-500 active:bg-blue-700`,
88+
text: tw`text-white`,
89+
},
90+
secondary: {
91+
container: tw`bg-gray-500 active:bg-gray-700`,
92+
text: tw`text-white`,
93+
},
94+
destructive: {
95+
container: tw`bg-red-500 active:bg-red-700`,
96+
text: tw`text-white`,
97+
},
98+
} satisfies Record<ButtonColor, { container: TwStyle<ViewStyle>; text: TwStyle<TextStyle> }>;
99+
100+
type ButtonProps = {
101+
title: string;
102+
size?: ButtonSize;
103+
color?: ButtonColor;
104+
onPress?: () => void;
105+
};
106+
107+
export function Button({ title, size = "md", color = "primary", onPress }: ButtonProps) {
108+
const sizeStyles = sizeVariants[size];
109+
const colorStyles = colorVariants[color];
110+
111+
return (
112+
<Pressable
113+
className="flex-row items-center justify-center"
114+
style={(state) => [
115+
sizeStyles.container.style,
116+
colorStyles.container.style,
117+
state.pressed && colorStyles.container.activeStyle,
118+
]}
119+
onPress={onPress}
120+
>
121+
<Text className="font-semibold" style={[sizeStyles.text.style, colorStyles.text.style]}>
122+
{title}
123+
</Text>
124+
</Pressable>
125+
);
126+
}
127+
```
128+
129+
## Complete Example: Button with Variants
130+
131+
Here's a full implementation combining size, color, and style variants:
132+
133+
```tsx
134+
import { type PropsWithChildren } from "react";
135+
import { Text, type StyleProp, type ViewStyle, type TextStyle } from "react-native";
136+
import { Pressable, tw, type PressableProps, type TwStyle } from "@mgcrea/react-native-tailwind";
137+
138+
// Types
139+
export type ButtonSize = "sm" | "md" | "lg";
140+
export type ButtonVariant = "solid" | "outline" | "ghost";
141+
export type ButtonColor = "primary" | "destructive" | "neutral";
142+
143+
// Size variants
144+
const sizeVariants = {
145+
sm: {
146+
container: tw`h-9 px-3 rounded-lg`,
147+
text: tw`text-sm`,
148+
},
149+
md: {
150+
container: tw`h-11 px-5 rounded-xl`,
151+
text: tw`text-base`,
152+
},
153+
lg: {
154+
container: tw`h-14 px-8 rounded-2xl`,
155+
text: tw`text-lg`,
156+
},
157+
} satisfies Record<ButtonSize, { container: TwStyle<ViewStyle>; text: TwStyle<TextStyle> }>;
158+
159+
// Style variants per color
160+
const variantStyles: Record<
161+
ButtonVariant,
162+
Record<ButtonColor, { container: TwStyle<ViewStyle>; text: TwStyle<TextStyle> }>
163+
> = {
164+
solid: {
165+
primary: {
166+
container: tw`bg-blue-500 active:bg-blue-700 disabled:bg-blue-300`,
167+
text: tw`text-white`,
168+
},
169+
destructive: {
170+
container: tw`bg-red-500 active:bg-red-700 disabled:bg-red-300`,
171+
text: tw`text-white`,
172+
},
173+
neutral: {
174+
container: tw`bg-gray-900 active:bg-gray-700 disabled:bg-gray-400`,
175+
text: tw`text-white`,
176+
},
177+
},
178+
outline: {
179+
primary: {
180+
container: tw`border border-blue-500 bg-white active:bg-blue-50`,
181+
text: tw`text-blue-500`,
182+
},
183+
destructive: {
184+
container: tw`border border-red-500 bg-white active:bg-red-50`,
185+
text: tw`text-red-500`,
186+
},
187+
neutral: {
188+
container: tw`border border-gray-300 bg-white active:bg-gray-100`,
189+
text: tw`text-gray-900`,
190+
},
191+
},
192+
ghost: {
193+
primary: {
194+
container: tw`active:bg-blue-50`,
195+
text: tw`text-blue-500`,
196+
},
197+
destructive: {
198+
container: tw`active:bg-red-50`,
199+
text: tw`text-red-500`,
200+
},
201+
neutral: {
202+
container: tw`active:bg-gray-100`,
203+
text: tw`text-gray-900`,
204+
},
205+
},
206+
};
207+
208+
// Props
209+
export type ButtonProps = Omit<PressableProps, "children" | "style"> & {
210+
title?: string;
211+
variant?: ButtonVariant;
212+
size?: ButtonSize;
213+
color?: ButtonColor;
214+
style?: StyleProp<ViewStyle>;
215+
textStyle?: StyleProp<TextStyle>;
216+
};
217+
218+
// Component
219+
export function Button({
220+
variant = "solid",
221+
size = "md",
222+
color = "primary",
223+
title,
224+
children,
225+
disabled,
226+
style,
227+
textStyle,
228+
...props
229+
}: PropsWithChildren<ButtonProps>) {
230+
const sizeStyles = sizeVariants[size];
231+
const colorStyles = variantStyles[variant][color];
232+
233+
return (
234+
<Pressable
235+
className="flex-row items-center justify-center"
236+
style={(state) => [
237+
sizeStyles.container.style,
238+
colorStyles.container.style,
239+
state.pressed && colorStyles.container.activeStyle,
240+
disabled && colorStyles.container.disabledStyle,
241+
style,
242+
]}
243+
disabled={disabled}
244+
{...props}
245+
>
246+
{children ?? (
247+
<Text
248+
className="font-semibold"
249+
style={[sizeStyles.text.style, colorStyles.text.style, textStyle]}
250+
>
251+
{title}
252+
</Text>
253+
)}
254+
</Pressable>
255+
);
256+
}
257+
```
258+
259+
### Usage
260+
261+
```tsx
262+
// Default: solid, medium, primary
263+
<Button title="Click Me" onPress={handlePress} />
264+
265+
// Outline destructive button
266+
<Button
267+
title="Delete"
268+
variant="outline"
269+
color="destructive"
270+
onPress={handleDelete}
271+
/>
272+
273+
// Large ghost button
274+
<Button
275+
title="Learn More"
276+
variant="ghost"
277+
size="lg"
278+
color="primary"
279+
onPress={handleLearnMore}
280+
/>
281+
282+
// Disabled state
283+
<Button
284+
title="Submitting..."
285+
disabled={isLoading}
286+
/>
287+
288+
// With custom style overrides
289+
<Button
290+
title="Custom"
291+
style={{ marginTop: 16 }}
292+
textStyle={{ letterSpacing: 1 }}
293+
/>
294+
```
295+
296+
## Key Points
297+
298+
### Type Safety with `satisfies`
299+
300+
Use TypeScript's `satisfies` operator to ensure your variant objects match the expected shape while preserving literal types:
301+
302+
```tsx
303+
const sizeVariants = {
304+
sm: { container: tw`h-9 px-3`, text: tw`text-sm` },
305+
md: { container: tw`h-11 px-5`, text: tw`text-base` },
306+
lg: { container: tw`h-14 px-8`, text: tw`text-lg` },
307+
} satisfies Record<ButtonSize, { container: TwStyle<ViewStyle>; text: TwStyle<TextStyle> }>;
308+
```
309+
310+
### Using TwStyle Properties
311+
312+
The `TwStyle` type provides typed access to modifier styles:
313+
314+
```tsx
315+
type TwStyle<T> = {
316+
style: T; // Base styles
317+
activeStyle?: T; // active: modifier
318+
focusStyle?: T; // focus: modifier
319+
disabledStyle?: T; // disabled: modifier
320+
hoverStyle?: T; // hover: modifier
321+
// ... other modifiers
322+
};
323+
```
324+
325+
Apply them conditionally based on component state:
326+
327+
```tsx
328+
<Pressable
329+
style={(state) => [
330+
styles.container.style,
331+
state.pressed && styles.container.activeStyle,
332+
state.focused && styles.container.focusStyle,
333+
disabled && styles.container.disabledStyle,
334+
]}
335+
>
336+
```
337+
338+
### Combining Multiple Style Sources
339+
340+
When combining styles from different variant dimensions, use array syntax:
341+
342+
```tsx
343+
style={(state) => [
344+
sizeStyles.container.style, // Size dimension
345+
colorStyles.container.style, // Color dimension
346+
state.pressed && colorStyles.container.activeStyle,
347+
style, // User overrides
348+
]}
349+
```
350+
351+
### Null-Safe Variants
352+
353+
For optional variant styles, use null checks:
354+
355+
```tsx
356+
const variantStyles = {
357+
link: {
358+
container: null, // No container styles for link variant
359+
text: tw`text-blue-500 underline`,
360+
},
361+
};
362+
363+
// In component
364+
style={[
365+
sizeStyles.container.style,
366+
colorStyles.container?.style, // Safe access
367+
state.pressed && colorStyles.container?.activeStyle,
368+
]}
369+
```
370+
371+
## What's Next?
372+
373+
- Learn about [Compile-Time tw](/react-native-tailwind/guides/compile-time-tw/) for the `tw` template basics
374+
- Explore [State Modifiers](/react-native-tailwind/guides/state-modifiers/) for interactive styling
375+
- Check out [Reusable Components](/react-native-tailwind/guides/reusable-components/) for component library patterns

0 commit comments

Comments
 (0)