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

PropTypeDefaultDescription
asScrollbooleanfalseWrap content in ScrollView
safeAreabooleantrueApply safe area insets
safeEdgesEdge[]autoWhich edges to protect
edgeToEdgebooleantrueContent extends under status bar
contentPaddingnumber/object20Content padding
tabBarPaddingbooleanfalseAdd bottom padding for tab bar
keyboardAwarebooleanfalseEnable KeyboardAvoidingView
keyboardDismissOnTapbooleanfalseDismiss keyboard on tap
variantstring"plain"Visual preset (see variants)
imageSourceImageSourcenullBackground image
blurIntensitynumbervariesBlur amount (iOS only)
imageOpacitynumbervariesBackground image opacity
refreshingboolean-Pull-to-refresh state
onRefreshfunction-Pull-to-refresh callback

Variants

VariantBlurOpacityUse Case
plain00.3Simple screens
immersive300.45Main app screens
hero200.35Hero sections
form100.2Forms with keyboard
glass400.3Glass 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

PageLocationHeader Icon
Settingsapp/(tabs)/settings.tsxsettings
Interviewapp/(tabs)/interview/index.tsxbriefcase
Practiceapp/(tabs)/practice/PracticeMenu.tsxbook
Resultsapp/(tabs)/results/index.tsxstats-chart
Quizapp/(tabs)/quiz/index.tsxhelp-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

  1. Always use theme colors - Never hardcode colors, use theme.primary, theme.text, etc.

  2. Consistent spacing - Use paddingHorizontal: 16 for sections, padding: 16 for card content

  3. Icon backgrounds - Use ${theme.primary}20 (20% opacity) for icon containers

  4. Dividers - Use borderBottomColor: theme.border between list items

  5. Dynamic padding - Use Math.max(insets.top + 16, 60) for header top padding

  6. No rainbow colors - Avoid multiple bright colors on same screen

  7. Consistent border radius - Cards: 16, Buttons: 14, Icons: 12, Badges: 6-8