Design System
Civix Design System
Overview
This document outlines the consistent design patterns used across the Civix app. All pages follow the Settings page style for visual consistency.
ThemeBackground Component
The ThemeBackground component is the foundation of all screens. It provides:
- Safe area handling - Automatic insets for notch/Dynamic Island
- Keyboard handling - KeyboardAvoidingView with proper offsets
- Background effects - Blurred images, gradients, vignettes
- Scroll support - Built-in ScrollView with pull-to-refresh
- Status bar - Automatic light/dark styling
Basic Usage
import ThemeBackground from "@/components/ThemeBackground";
<ThemeBackground
safeEdges={["left", "right", "bottom"]}
contentPadding={0}
imageSource={require("@/assets/images/splashcivic-fast.jpg")}
variant="immersive"
blurIntensity={6}
imageOpacity={0.7}
>
{/* Your content */}
</ThemeBackground>
Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
asScroll | boolean | false | Wrap content in ScrollView |
safeArea | boolean | true | Apply safe area insets |
safeEdges | Edge[] | auto | Which edges to protect |
edgeToEdge | boolean | true | Content extends under status bar |
contentPadding | number/object | 20 | Content padding |
tabBarPadding | boolean | false | Add bottom padding for tab bar |
keyboardAware | boolean | false | Enable KeyboardAvoidingView |
keyboardDismissOnTap | boolean | false | Dismiss keyboard on tap |
variant | string | "plain" | Visual preset (see variants) |
imageSource | ImageSource | null | Background image |
blurIntensity | number | varies | Blur amount (iOS only) |
imageOpacity | number | varies | Background image opacity |
refreshing | boolean | - | Pull-to-refresh state |
onRefresh | function | - | Pull-to-refresh callback |
Variants
| Variant | Blur | Opacity | Use Case |
|---|---|---|---|
plain | 0 | 0.3 | Simple screens |
immersive | 30 | 0.45 | Main app screens |
hero | 20 | 0.35 | Hero sections |
form | 10 | 0.2 | Forms with keyboard |
glass | 40 | 0.3 | Glass morphism effect |
Utility Wrappers
// Standard screen wrapper
<ScreenWrapper scroll keyboard dismissKeyboard>
// Form with keyboard handling
<FormBackground>
// Hero section
<HeroBackground>
// Tab screen with bottom padding
<TabScreenBackground>
Color System
Theme Colors (from lib/theme.ts)
// Light Mode
primary: "#2563eb" // Blue - main accent
success: "#16a34a" // Green - passed/correct
error: "#dc2626" // Red - failed/wrong
warning: "#d97706" // Orange - alerts
info: "#0284c7" // Light blue - info
// Dark Mode
primary: "#60a5fa" // Lighter blue
success: "#22c55e" // Brighter green
error: "#f87171" // Lighter red
Header Gradients
// Dark Mode Header
colors={["#1e3a5f", "#0f172a"]}
// Light Mode Header
colors={["#6366f1", "#4f46e5"]}
Accent Colors (ACCENT constant)
gold: "#D4AF37" // Premium/special highlights
goldLight: "#F4E4BC"
goldDark: "#B8860B"
gradientGold: ["#D4AF37", "#B8860B"]
Page Structure
Every main page follows this structure:
<ThemeBackground
safeEdges={["left", "right", "bottom"]}
contentPadding={0}
imageSource={require("@/assets/images/splashcivic-fast.jpg")}
variant="immersive"
blurIntensity={6}
imageOpacity={0.7}
>
<ScrollView>
{/* 1. Gradient Header */}
<LinearGradient colors={isDark ? ["#1e3a5f", "#0f172a"] : ["#6366f1", "#4f46e5"]}>
<View style={styles.headerIcon}>
<Ionicons name="..." size={28} color="#fff" />
</View>
<AppText variant="h2" weight="bold">Title</AppText>
<AppText>Subtitle</AppText>
</LinearGradient>
{/* 2. Sections */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Ionicons name="..." size={20} color={theme.primary} />
<AppText weight="bold">Section Title</AppText>
</View>
<View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }]}>
{/* Content rows */}
</View>
</View>
{/* 3. Footer */}
<View style={styles.footer}>
<AppText variant="caption" muted center>Footer text</AppText>
</View>
</ScrollView>
</ThemeBackground>
Standard Styles
Header
header: {
paddingBottom: 24,
paddingHorizontal: 20,
alignItems: "center",
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
},
headerIcon: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: "rgba(255,255,255,0.2)",
justifyContent: "center",
alignItems: "center",
marginBottom: 12,
},
headerTitle: { color: "#fff" },
headerSubtitle: { color: "rgba(255,255,255,0.8)", marginTop: 4, fontSize: 14 },
Sections
section: { paddingHorizontal: 16, marginTop: 20 },
sectionHeader: {
flexDirection: "row",
alignItems: "center",
gap: 8,
marginBottom: 10,
paddingHorizontal: 4
},
sectionTitle: { fontSize: 15 },
Cards
card: {
borderRadius: 16,
borderWidth: 1,
overflow: "hidden"
},
// Usage:
<View style={[styles.card, {
backgroundColor: theme.card,
borderColor: theme.border
}]}>
List Rows
optionRow: {
flexDirection: "row",
alignItems: "center",
padding: 16,
gap: 14,
},
optionIcon: {
width: 44,
height: 44,
borderRadius: 12,
justifyContent: "center",
alignItems: "center",
},
optionText: { flex: 1 },
// Usage with dividers:
<Pressable style={[
styles.optionRow,
!isLast && { borderBottomWidth: 1, borderBottomColor: theme.border }
]}>
<View style={[styles.optionIcon, { backgroundColor: `${theme.primary}20` }]}>
<Ionicons name="..." size={22} color={theme.primary} />
</View>
<View style={styles.optionText}>
<AppText weight="medium">Title</AppText>
<AppText variant="caption" muted>Description</AppText>
</View>
<Ionicons name="chevron-forward" size={20} color={theme.subtext} />
</Pressable>
Component Patterns
Stats Grid
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Ionicons name="..." size={24} color={theme.primary} />
<AppText weight="bold" style={{ fontSize: 18 }}>{value}</AppText>
<AppText variant="caption" muted>Label</AppText>
</View>
{/* More stat items */}
</View>
// Styles:
statsGrid: { flexDirection: "row", paddingVertical: 16 },
statItem: { flex: 1, alignItems: "center", gap: 4 },
Info/Warning Banner
<View style={[styles.infoCard, {
backgroundColor: theme.warningLight,
borderColor: theme.warning
}]}>
<Ionicons name="information-circle" size={20} color={theme.warning} />
<AppText style={{ color: theme.warning, flex: 1, fontSize: 13 }}>
Message text
</AppText>
</View>
// Styles:
infoCard: {
flexDirection: "row",
alignItems: "center",
gap: 12,
padding: 14,
borderRadius: 14,
borderWidth: 1,
},
Status Badge
<View style={[styles.statusBadge, {
backgroundColor: isPassed ? theme.successLight : theme.errorLight
}]}>
<AppText variant="caption" weight="bold" style={{
color: isPassed ? theme.success : theme.error
}}>
{isPassed ? "Passed" : "Failed"}
</AppText>
</View>
// Styles:
statusBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 },
Pages Using This System
| Page | Location | Header Icon |
|---|---|---|
| Settings | app/(tabs)/settings.tsx | settings |
| Interview | app/(tabs)/interview/index.tsx | briefcase |
| Practice | app/(tabs)/practice/PracticeMenu.tsx | book |
| Results | app/(tabs)/results/index.tsx | stats-chart |
| Quiz | app/(tabs)/quiz/index.tsx | help-circle |
Special Pages
Onboarding (app/onboarding.tsx)
- Uses ImageBackground without blur for dramatic first impression
- Custom styling appropriate for one-time onboarding flow
Paywall (app/(tabs)/paywall.tsx)
- Uses ImageBackground with gradient overlay
- Premium feel with fully visible background image
Typography
Using AppText component with variants:
<AppText variant="display"> // 32px - Main titles
<AppText variant="h1"> // 28px - Page titles
<AppText variant="h2"> // 22px - Section titles
<AppText variant="h3"> // 18px - Card titles
<AppText variant="body"> // 16px - Default
<AppText variant="sub"> // 14px - Descriptions
<AppText variant="caption"> // 12px - Labels, timestamps
<AppText variant="overline"> // 10px - Small labels
// Weights
weight="bold" | "medium" | "regular"
// Props
muted // Uses theme.subtext color
center // Centers text
Best Practices
-
Always use theme colors - Never hardcode colors, use
theme.primary,theme.text, etc. -
Consistent spacing - Use
paddingHorizontal: 16for sections,padding: 16for card content -
Icon backgrounds - Use
${theme.primary}20(20% opacity) for icon containers -
Dividers - Use
borderBottomColor: theme.borderbetween list items -
Dynamic padding - Use
Math.max(insets.top + 16, 60)for header top padding -
No rainbow colors - Avoid multiple bright colors on same screen
-
Consistent border radius - Cards: 16, Buttons: 14, Icons: 12, Badges: 6-8