User-defined themes in FlutterFlow require a color picker Custom Action using flutter_colorpicker, font selection via a dropdown, a dark mode toggle stored in App State, and Firestore sync so preferences persist across sessions. Always validate contrast ratios after color changes — low-contrast choices (yellow on white) make text unreadable and fail accessibility standards. Cache preferences in App State to apply the theme before Firestore loads.
Why User-Defined Themes Increase Retention
Personalization is one of the strongest drivers of app retention. Users who customize their app's appearance engage 35% longer per session and have higher 30-day retention rates. In FlutterFlow, implementing user-defined themes involves three components: a preferences UI (color picker, font selector, dark mode toggle), an App State layer that holds the active theme and applies it globally, and a Firestore persistence layer that saves and restores preferences across sessions. The App State layer is critical for instant theme application — without it, users see a flash of the default theme on every app launch before Firestore loads their preferences.
Prerequisites
- A FlutterFlow project with Firebase connected and an authenticated users collection in Firestore
- FlutterFlow Pro plan for Custom Actions (required for flutter_colorpicker integration)
- The flutter_colorpicker package added to your exported project's pubspec.yaml
- An existing theme defined in FlutterFlow's Theme Settings as the base/default theme
Step-by-step guide
Create App State variables for the active user theme
Create App State variables for the active user theme
In FlutterFlow, open App State (left sidebar → App State icon). Create the following variables: 'primaryColor' (String, default '#6366F1'), 'secondaryColor' (String, default '#8B5CF6'), 'fontFamily' (String, default 'Roboto'), 'isDarkMode' (Boolean, default false), 'textColor' (String, default '#1A1A2E'), 'backgroundColor' (String, default '#FFFFFF'). These App State variables are the single source of truth for the active theme throughout the app session. Mark all of them as 'Persisted' in FlutterFlow's App State panel — this enables local storage persistence so the theme survives app restarts even before Firestore loads.
Expected result: App State panel shows all 6 theme variables with their default values and 'Persisted' enabled.
Build the theme settings screen
Build the theme settings screen
Create a new page named 'Theme Settings'. Add a ListTile for 'Primary Color' — a label on the left and a circular Container on the right showing the current primaryColor from App State. Repeat for 'Secondary Color' and 'Text Color'. Add a Row for 'Dark Mode' with a label and a Toggle widget bound to the isDarkMode App State variable. Add a 'Font Style' row with a DropdownButton offering 4-6 font options (Roboto, Lato, Nunito, Playfair Display, Source Code Pro). Add a live preview section at the bottom showing a sample heading, body text paragraph, and primary button using the current App State theme values — so users see changes in real time before saving.
Expected result: The theme settings page renders with current theme values pre-populated and a live preview section visible below the controls.
Implement the color picker with contrast validation
Implement the color picker with contrast validation
Create a Custom Action named 'pickColorWithValidation'. This action takes a currentHex parameter and a targetElement parameter ('primary', 'background', or 'text'). It opens the flutter_colorpicker dialog. After the user picks a color and closes the dialog, the action computes the contrast ratio between the new primary color and the current background color using the WCAG formula. If the contrast ratio is below 4.5:1 (WCAG AA standard for normal text), show a SnackBar warning: 'Low contrast — text may be hard to read. Try a darker or lighter color.' Still allow the user to keep the selection if they choose. Return the selected hex string regardless so the caller can apply it.
1// Custom Action: pickColorWithValidation2import 'package:flutter/material.dart';3import 'package:flutter_colorpicker/flutter_colorpicker.dart';45Future<String> pickColorWithValidation(6 BuildContext context,7 String currentHex,8 String backgroundHex,9) async {10 Color picked = _hexToColor(currentHex);11 await showDialog(12 context: context,13 builder: (ctx) => AlertDialog(14 title: const Text('Choose color'),15 content: SingleChildScrollView(16 child: ColorPicker(17 pickerColor: picked,18 onColorChanged: (c) => picked = c,19 hexInputBar: true,20 enableAlpha: false,21 ),22 ),23 actions: [24 TextButton(25 onPressed: () => Navigator.pop(ctx),26 child: const Text('Select'),27 ),28 ],29 ),30 );31 // Validate contrast against background32 final bg = _hexToColor(backgroundHex);33 final ratio = _contrastRatio(picked, bg);34 if (ratio < 4.5 && context.mounted) {35 ScaffoldMessenger.of(context).showSnackBar(36 SnackBar(37 content: Text(38 'Contrast ratio ${ratio.toStringAsFixed(1)}:1 — may be hard to read (WCAG AA requires 4.5:1)',39 ),40 backgroundColor: Colors.orange,41 ),42 );43 }44 return _colorToHex(picked);45}4647Color _hexToColor(String hex) =>48 Color(int.parse(hex.replaceFirst('#', '0xFF')));4950String _colorToHex(Color c) =>51 '#${c.value.toRadixString(16).substring(2).toUpperCase()}';5253double _contrastRatio(Color fg, Color bg) {54 final l1 = fg.computeLuminance();55 final l2 = bg.computeLuminance();56 final lighter = l1 > l2 ? l1 : l2;57 final darker = l1 > l2 ? l2 : l1;58 return (lighter + 0.05) / (darker + 0.05);59}Expected result: The color picker dialog opens, and selecting a low-contrast color shows an orange warning snackbar with the specific contrast ratio.
Sync theme preferences to Firestore on save
Sync theme preferences to Firestore on save
Add a 'Save Theme' button to the bottom of the Theme Settings page. In the button's Action Flow: Action 1 — Update App State to apply all current selections to the App State variables (they will persist locally via persisted App State). Action 2 — Update Firestore Document (users/{currentUserUid}) with a Map field 'themePreferences' containing all theme values as key-value pairs: {primaryColor, secondaryColor, fontFamily, isDarkMode, textColor, backgroundColor}. Action 3 — Show a success Snackbar 'Theme saved'. The App State update is instant; Firestore is the backup for cross-device sync.
Expected result: Saving the theme updates App State immediately (widgets respond) and writes to Firestore (visible in Firebase Console) within 1-2 seconds.
Restore theme preferences on app launch
Restore theme preferences on app launch
In your app's initial route page (or MainPage), add an On Page Load action flow. After authentication check, add a Custom Action named 'loadUserTheme'. This action reads the user's Firestore document, extracts the 'themePreferences' Map, and updates each App State variable. Because App State is persisted locally, the theme from the previous session is already applied from local storage — the Firestore fetch updates it to the latest value (in case the user changed theme on another device). This two-phase approach (local cache first, Firestore update second) eliminates the flash of default theme on launch.
1// Custom Action: loadUserTheme2import 'package:cloud_firestore/cloud_firestore.dart';3import 'package:firebase_auth/firebase_auth.dart';4// Note: Update FFAppState fields using FlutterFlow's generated AppState class56Future<void> loadUserTheme() async {7 final user = FirebaseAuth.instance.currentUser;8 if (user == null) return;9 final doc = await FirebaseFirestore.instance10 .collection('users')11 .doc(user.uid)12 .get();13 final prefs =14 doc.data()?['themePreferences'] as Map<String, dynamic>?;15 if (prefs == null) return;16 // Update FlutterFlow App State variables17 // (use FFAppState().update() in exported code)18 FFAppState().update(() {19 if (prefs['primaryColor'] != null)20 FFAppState().primaryColor = prefs['primaryColor'];21 if (prefs['secondaryColor'] != null)22 FFAppState().secondaryColor = prefs['secondaryColor'];23 if (prefs['fontFamily'] != null)24 FFAppState().fontFamily = prefs['fontFamily'];25 if (prefs['isDarkMode'] != null)26 FFAppState().isDarkMode = prefs['isDarkMode'];27 if (prefs['backgroundColor'] != null)28 FFAppState().backgroundColor = prefs['backgroundColor'];29 });30}Expected result: On app launch, the user sees their previously saved theme colors and font immediately, with no visible flash to the default theme.
Complete working example
1// Theme utilities: contrast validation, color conversion, font mapping2// Add to FlutterFlow Custom Code panel34import 'package:flutter/material.dart';56// ─── Convert hex string to Flutter Color ────────────────────────────────────7Color hexToColor(String hex) {8 final clean = hex.replaceFirst('#', '');9 return Color(int.parse('FF$clean', radix: 16));10}1112// ─── Convert Flutter Color to hex string ────────────────────────────────────13String colorToHex(Color color) {14 return '#${color.value.toRadixString(16).substring(2).toUpperCase()}';15}1617// ─── Compute WCAG contrast ratio ────────────────────────────────────────────18double contrastRatio(String foregroundHex, String backgroundHex) {19 final fg = hexToColor(foregroundHex);20 final bg = hexToColor(backgroundHex);21 final l1 = fg.computeLuminance();22 final l2 = bg.computeLuminance();23 final lighter = l1 > l2 ? l1 : l2;24 final darker = l1 > l2 ? l2 : l1;25 return (lighter + 0.05) / (darker + 0.05);26}2728// ─── Check if white or dark text is more readable on a background ────────────29bool useWhiteTextOn(String backgroundHex) {30 final bg = hexToColor(backgroundHex);31 return bg.computeLuminance() < 0.4;32}3334// ─── Get WCAG compliance level ───────────────────────────────────────────────35String wcagLevel(double ratio) {36 if (ratio >= 7.0) return 'AAA';37 if (ratio >= 4.5) return 'AA';38 if (ratio >= 3.0) return 'AA Large';39 return 'Fail';40}4142// ─── Generate complementary accent color ─────────────────────────────────────43String complementaryColor(String primaryHex) {44 final color = hexToColor(primaryHex);45 final hslColor = HSLColor.fromColor(color);46 final complementary = hslColor.withHue(47 (hslColor.hue + 180) % 360,48 );49 return colorToHex(complementary.toColor());50}5152// ─── Suggest readable text color for a background ────────────────────────────53String suggestTextColor(String backgroundHex) {54 return useWhiteTextOn(backgroundHex) ? '#FFFFFF' : '#1A1A2E';55}5657// ─── Map font name to FlutterFlow font identifier ────────────────────────────58String fontFamilyLabel(String fontKey) {59 const map = {60 'Roboto': 'Clean & modern (default)',61 'Lato': 'Friendly & open',62 'Nunito': 'Rounded & playful',63 'Playfair Display': 'Elegant & editorial',64 'Source Code Pro': 'Technical & monospace',65 };66 return map[fontKey] ?? fontKey;67}Common mistakes
Why it's a problem: Applying user-selected theme colors without checking contrast ratio first
How to avoid: Always compute the WCAG contrast ratio between text color and background color after any color selection. Show a warning if the ratio is below 4.5:1 and suggest adjusting lightness to meet the standard.
Why it's a problem: Loading the user's Firestore theme preference before displaying any UI, causing a flash and delay
How to avoid: Use FlutterFlow's Persisted App State to store the last-known theme locally. On launch, the locally cached theme applies instantly. The Firestore read runs in the background and updates App State only if the values differ (e.g., theme was changed on another device).
Why it's a problem: Offering too many font choices, including decorative fonts for body text
How to avoid: Curate 4-6 carefully selected body-safe font options. For each, show a preview sentence at 14px to help users make informed choices. Separate display fonts (for headings only) from body fonts if you offer both.
Best practices
- Always validate WCAG contrast ratio (minimum 4.5:1) after any color selection and display the ratio value so users understand the feedback.
- Provide a 'Reset to Default' button on the theme settings screen so users can recover from a bad combination they cannot read.
- Cache user theme in Persisted App State so the theme applies from local storage on cold launch with zero delay before Firestore loads.
- Preview font changes at multiple sizes (heading, body, caption) in a live preview panel — a font that looks great at 24px may be unreadable at 12px.
- When dark mode is toggled, automatically compute a dark-mode-appropriate version of the user's custom primary color rather than just inverting all colors.
- Limit the primary color picker to a curated palette or add a saturation minimum — very desaturated (near-grey) primary colors produce flat, unprofessional interfaces.
- Test user-defined themes on low-contrast display conditions (bright outdoor sunlight) by reducing screen brightness to minimum during testing.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a user-defined theme system in a FlutterFlow app. Users pick a primary color, background color, and font family. I need to validate that the selected colors meet WCAG AA contrast standards. Explain the WCAG contrast ratio formula, write me a Dart function that computes the contrast ratio between two hex color strings, and suggest how to automatically adjust a failing color to meet the 4.5:1 threshold.
In my FlutterFlow app, I have App State variables: primaryColor (String hex), backgroundColor (String hex), and isDarkMode (Boolean). I want every button in my app to use the primaryColor with dynamically computed text color (white if dark background, black if light). How do I use FlutterFlow's conditional styling to apply these App State values to a Button widget's fill color and text color properties?
Frequently asked questions
Can I change the theme globally across the entire app without rebuilding every widget?
In FlutterFlow, App State variables are reactive — any widget bound to an App State variable re-renders automatically when the value changes. For a true global theme, every color and font reference in your widgets should use App State variables rather than hard-coded values. Changing an App State theme variable instantly updates all bound widgets across all pages.
How do I let users switch between preset themes (e.g., Ocean, Forest, Sunset) in addition to custom colors?
Create a Map constant in FlutterFlow (or a Firestore 'themes' collection) with preset theme definitions. Each preset is a Map with keys: primaryColor, secondaryColor, backgroundColor, textColor. When a user selects a preset, update all App State theme variables to the preset values in a single action flow. This triggers a global re-render to the new preset immediately.
Does FlutterFlow support system-level dark mode (following the device's dark/light setting)?
FlutterFlow has built-in dark mode support in its Theme Settings. If you want to follow the system setting automatically, use a Custom Action on app launch that reads MediaQuery.platformBrightnessOf(context) and sets your isDarkMode App State variable accordingly. Re-check on every app resume using the WidgetsBindingObserver lifecycle callbacks.
How do I ensure font changes apply to custom Text widgets that have their font hard-coded?
Any Text widget with a hard-coded font family in its Text Style will not respond to App State font changes. To make fonts dynamic, clear the hard-coded font family in the widget's text style and instead use a conditional expression that reads from your fontFamily App State variable. This requires reviewing every Text widget in your project — a significant refactor if done after the fact.
Can theme preferences sync across multiple devices for the same user?
Yes, via Firestore. When a user saves their theme on Device A, it writes to their Firestore user document. When they open the app on Device B, the loadUserTheme action reads the latest Firestore values and updates App State. If you want real-time sync (changes reflect immediately across open sessions), set up a Firestore real-time listener on the user document's themePreferences field.
What contrast ratio should I target for my app to be accessible?
WCAG 2.1 AA (the industry standard for mobile apps) requires 4.5:1 for normal text and 3:1 for large text (18px+ regular or 14px+ bold). WCAG AAA requires 7:1 for normal text. Apple and Google both reference these standards in their human interface guidelines. Aim for AA compliance as a minimum, and test your most common color combinations with a contrast checker before shipping.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation