This Scalable Card Component Fixed My Messy React Native UI
My UI became easier to scale once I started thinking in systems
This Scalable Card Component Fixed My Messy React Native UI
My UI became easier to scale once I started thinking in systems

The image is created by Author using app screenshots and AI
Let me tell you about a mistake I was making in my new React Native app.
I was building an app, let's say a coffee shop app. It had many screens and components, including product cards, review cards, settings cards, and stats cards.
Every time I needed a new screen or a card, I opened AI tool, gave it a context, and asked for a “nice design”. Paste the AI generated code result by adjusting a few things.
Sounds fine, right?
It wasn’t.
This approach creates 2 issues.
Inconsistent Design
After a few days, I opened the project to work on a new feature, and I realized something was wrong.
Everything still worked. But it just didn’t feel right.
The design of app was inconsistent.
Then I dug deep, and I figured it out.
Every card in my app was slightly different from the others:
- One card had
borderRadius: 12. Another hadborderRadius: 16. - One used
padding: 16. Another usedpadding: 12. - Background colors were all over the place,
#fffhere,#F9FAFBthere,whitesomewhere else. - Shadows? Each card had its own thing going on.
Each card looked fine on its own. But together? The app looked like it was built by five different people who never talked to each other.
That’s when I realized I wasn’t building a design system. I was just building screens and hoping for the best.
Why Does This Happen?
When we try to build fast, we just want things to work.
When I need a new card. I copy one from another screen, change a few styles, and ship it.
And if you’re using AI to help you build UI (like I was), the problem gets even bigger. AI is great at building UI. But it doesn’t know your app. It doesn’t know your design has borderRadius: 16. It doesn't know your shadow color. So every time you ask it to build a card or UI, it makes up its own style.
The result? An inconsistent app where Everything works, but nothing matches.
Dark Mode Becomes a Nightmare
The second problem was that it became difficult to implement dark mode efficiently.
I decided to add dark mode.
First, it seems easy, just swap some colors.
But due to my current workflow, it wasn’t that easy.
Here’s what actually happened:
- I opened the first card. It had
backgroundColor: '#fff'written directly in the code. - I opened the second card. Same thing, but a different color value.
- The third card had its border color hardcoded too.
- The fourth card had its own shadow settings.
Every single card had its own hardcoded styles.
To add dark mode, I had to go into every card one by one, change the background, fix the border, adjust the shadow, and test it all again. It took forever. And I kept missing things.
That’s when I asked myself: Why don’t I have one single Card component that defines my design system, and I use it everywhere?
React Native Card UI
In every serious app, we build reusable components, for examples one Button component and use it everywhere. We don't write a brand new button with its own styles on every screen.
Same with TextInput. One component, used everywhere.
So why are we building a new card from scratch every time?
Cards are probably the most common UI element in any mobile app. If you’re not controlling them from one place, your design will slowly fall apart, and you won’t even notice until it’s too messy.
The fix is simple:
One
Cardwrapper component for all cards in your app.
Let’s Build It
Here’s what I wanted my Card component to do:
- Control the background color
- Control the border and rounded corners
- Control the spacing inside the card (padding)
- Handle shadows properly on both iOS and Android
- Become tappable when needed
Here’s the full component code:
import { ReactNode } from 'react'
import { Platform, Pressable, StyleSheet, View, ViewStyle } from 'react-native'
type CardVariant = 'outlined' | 'filled' | 'elevated'
type CardPadding = 'none' | 'small' | 'medium' | 'large'
type CardProps = {
children: ReactNode
variant?: CardVariant
padding?: CardPadding
onPress?: () => void
style?: ViewStyle
disabled?: boolean
testID?: string
}
const COLORS = {
card: '#FFFFFF',
border: '#E5E7EB',
filled: '#F9FAFB',
pressed: '#F3F4F6',
shadow: '#64748b',
}
const PADDING: Record<CardPadding, number> = {
none: 0,
small: 12,
medium: 16,
large: 20,
}
export default function Card({
children,
variant = 'outlined',
padding = 'medium',
onPress,
style,
disabled = false,
testID,
}: CardProps) {
const baseStyle: ViewStyle = {
borderRadius: 16,
padding: PADDING[padding],
overflow: 'hidden',
}
const variantStyles: Record<CardVariant, ViewStyle> = {
outlined: {
backgroundColor: COLORS.card,
borderWidth: 1,
borderColor: COLORS.border,
},
filled: {
backgroundColor: COLORS.filled,
},
elevated: {
backgroundColor: COLORS.card,
...Platform.select({
ios: {
shadowColor: COLORS.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.12,
shadowRadius: 12,
},
android: {
elevation: 6,
shadowColor: COLORS.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
},
}),
},
}
const disabledStyle: ViewStyle = disabled ? { opacity: 0.5 } : {}
if (onPress) {
return (
<Pressable
testID={testID}
disabled={disabled}
onPress={onPress}
style={({ pressed }) => [
baseStyle,
variantStyles[variant],
pressed && styles.pressed,
disabledStyle,
style,
]}
>
{children}
</Pressable>
)
}
return (
<View testID={testID} style={[baseStyle, variantStyles[variant], disabledStyle, style]}>
{children}
</View>
)
}
const styles = StyleSheet.create({
pressed: {
opacity: 0.7,
transform: [{ scale: 0.98 }],
},
})
Now, let me explain the important parts in simple terms.
1. The variant prop — Three card styles
Instead of every card inventing its own random styles, you pick from three options:
**outlined** — White background with a light grey border. Great for settings rows or list items.**filled** — Light grey background, no border. Good for subtle, low-key cards.**elevated** — White background with a shadow underneath. Perfect for product cards or anything you want to pop off the screen.
2. The padding prop — Spacing tokens
Instead of guessing a random number, you pick a name:
none→ 0px inside the cardsmall→ 12pxmedium→ 16px (the default)large→ 20px
No more padding: 14 in one card and padding: 15 in another. Everyone picks from the same list.
3. The onPress prop — Tappable cards
Pass an onPress and the card becomes tappable. It even shrinks slightly when pressed (that's the scale: 0.98 at the bottom).
4. Shadows on iOS and Android
Here’s something that trips a lot of people up. Shadows work completely differently on iOS and Android:
- iOS needs:
shadowColor,shadowOffset,shadowOpacity,shadowRadius - Android needs: just
elevation(and optionallyshadowColor)
If you write shadow code for one platform, it breaks on the other. The Platform.select inside the elevated variant handles both automatically. You pick variant="elevated" and it just looks right everywhere.
5. The style prop — For custom sylting
The component gives you good defaults, but you can always override them.
If one specific card needs something custom, a different margin, or a specific width, you pass a style prop, and it gets layered on top.
Before vs After
Here’s what my product card looked like before I had the Card component:
<View
style={{
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
}}
>
<Image source={product.image} />
<Text>{product.name}</Text>
<Text>{product.price}</Text>
</View>
And after:
<Card variant="elevated">
<Image source={product.image} />
<Text>{product.name}</Text>
<Text>{product.price}</Text>
</Card>
Did you observe that the code becomes cleaner?
But the real benefit is when you need to change something, it’s no longer stressful.
Want to update the border radius for every card in the app? Change it in one place.
Want stronger or softer shadows? Adjust a single value.
Want to support dark mode? Update one color object.
Just one change and the whole app UI updates.
But What If I Need Different Cards?
Now you may think,
“Won’t one Card component make everything look the same?”
No.
Having one component doesn’t mean every card becomes identical. It just means the structure and default behavior stay consistent. The shape, spacing system, and styling rules are controlled in one place.
You handle those intentionally through props.
In my coffee shop app, for example, I use the same Card component everywhere, but I adjust it depending on the need.
Here’s one case:
A full-image banner, no inner padding needed:
<Card variant="elevated" padding="none">
<Image source={banner.image} style={{ width: '100%', height: 200 }} />
</Card>
A settings row with a simple border:
<Card variant="outlined" padding="small">
<SettingsRow label="Notifications" />
</Card>
A stats card that opens a new screen when tapped:
<Card variant="filled" onPress={() => router.push('/stats')}>
<StatDisplay value={totalOrders} label="Total Orders" />
</Card>
The variations are intentional. If you like, you can add more.
Making Dark Mode Actually Easy
It also solved our dark mode or multiple theme problem.
Remember how adding dark mode felt like a work even for AI (waste tokens)? Here’s how the Card component makes it a non-issue.
Step 1 — Create a small theme hook:
// useTheme.ts
import { useColorScheme } from 'react-native'
export function useTheme() {
const colorScheme = useColorScheme()
return {
card: colorScheme === 'dark' ? '#1F2937' : '#FFFFFF',
border: colorScheme === 'dark' ? '#374151' : '#E5E7EB',
filled: colorScheme === 'dark' ? '#111827' : '#F9FAFB',
shadow: '#64748b',
}
}
Step 2 — Use it inside the Card component:
export default function Card({ children, variant = 'outlined', padding = 'medium', ...rest }: CardProps) {
const colors = useTheme() // 👈 reads light or dark theme automatically
const variantStyles: Record<CardVariant, ViewStyle> = {
outlined: {
backgroundColor: colors.card, // updates automatically
borderWidth: 1,
borderColor: colors.border, // updates automatically
},
filled: {
backgroundColor: colors.filled, // updates automatically
},
elevated: {
backgroundColor: colors.card,
// ... shadows stay the same
},
}
// rest of the component stays exactly the same
}
Now, when the user switches to dark mode, every card in your entire app updates on its own. No hunting through screens.
That’s the whole point of having one reusable component.
Context for AI
Here’s one more benefit of it.
Once I had the Card component, AI became way more useful for building screens.
Before, when I asked AI to “build a product card,” it would make up its own styles, which were different every time.
But now I tell it: “Build a product card using our Card component with variant='elevated'."
And it does exactly that. It works inside my system instead of inventing its own, and the output just fits.
AI works best when you give it a clear structure. Without it, AI guesses and creates inconsistency. With proper building blocks like a Card component, it follows your system instead of inventing new styles, and that’s when it truly becomes helpful.
When your app is small, none of this feels important. You copy a style, change a color, ship it. Life is good.
But often apps don’t stay small. You keep adding new screens, new features, and new sections. And every hardcoded style you write today becomes something you’ll have to search for and fix later. The more you delay organizing it, the bigger and messier it becomes.
Did you learn something new? Let me know in the comment box.
메타데이터
- post_id
- 7e4e5cdda2ac
- slug
- my-react-native-ui-was-a-mess-until-i-built-this-one-card-component-7e4e5cdda2ac
- url
- https://blog.stackademic.com/my-react-native-ui-was-a-mess-until-i-built-this-one-card-component-7e4e5cdda2ac
- canonical_url
- https://blog.stackademic.com/my-react-native-ui-was-a-mess-until-i-built-this-one-card-component-7e4e5cdda2ac
- author_url
- https://medium.com/@basit.miyanjee
- status
- ok
- fetched_at
- 2026-06-09 15:37:30