Color Design Tokens for Open edX Mobile

Color Design Tokens for Open edX Mobile

Introduction

This document explores the current state of theming for iOS and Android vs web, and the path towards using design tokens for colors in the mobile apps.

Problem

Currently, iOS and Android each have a related but disparate set of colors; these colors also differ from the designs set in the Figma.

The Figma file currently has 22 distinct colors; iOS has 45, and Android has 42. On top of having different primitive color values, each app also has different sets of color variables. The drift between the two apps and with the Figma source of truth results in two main problems:

  • Accessibility Issues Color selection that strays from Figma results in difficult-to-manage accessibility issues. For example, secondary text color values differ between Android and iOS, and neither secondary text definitions match the colors defined in Figma: #3D4964. This is an issue because both sets of light-theme colors fail to meet minimum WCAG contrast ratios.

  • Maintenance Burden iOS has 77 color assets, all independently defined in the Assets.xcasette catalog. This means if a base color is changed, the color must be adjusted multiple times. For example: the Figma Accent color is Android has 75 color assets, and 42 unique colors total. The colors and variable names are different across platforms. As an example: iOS has textSecondary textSecondaryLight and textSecondaryDark, each with light and dark definitions; Android has a single text_secondary with matching #B3B3B3for light and dark mode. To further complicate things, the textSecondary color is used for non-text cases such as icons in some empty state screens. For any color definition changes, there will be multiple unintended color changes.

Previous Work

This proposal builds on the proposal https://openedx.atlassian.net/wiki/x/MoDrPQE from @Ivan Stepanok with design system/UX considerations. Schema and Raccoon Gang will align on the path forward.

Proposed Solution

This effort aims to create a unified set of colors and token names for mobile that relies on Paragon color tokens for the majority of primitive colors, with overrides to maintain the current mobile app styling while allowing further future alignment. The effort will reduce drift among the Figma, iOS, and Android code bases and improve maintainability. A shift to standardized token definitions will make future changes much easier to manage and will allow teams without mobile capacity to easily customize the color theming.

Added benefit: AI assisted UI development will benefit greatly from well-defined semantic variables and well defined color sets. Less AI guessing and made up colors.

Color Resolution

Paragon holds primitive token names and values. Paragon will also hold mobile primitive overrides (if necessary) and mobile semantic alias files. Style Dictionary will generate the necessary iOS / Android default token files.

Apps will consume generated files at build time. Any custom overrides/ theming will happen at run-time through primitive token overrides, keeping semantic/component tokens stable.

Staging

Phase 0: Fast track accessibility fix

Current secondary text color variables do not meet minimum a11y contrast ratios. Similarly, Form fields and associated placeholder text also need fixing. Fast track those fixes.

Current state: PR opened for iOS textSecondary / textSecondaryLight fixes here: fix: update secondary text color tokens by edschema · Pull Request #658 · openedx/openedx-app-ios

Once that PR is reviewed and merged, we will push the same fix for Android.

Fixes to input fields and placeholder text are in progress.

Phase 1: Inventory and cross-platform token mapping

In this phase, we plan to catalog all of the colors used in both apps and align colors and variable names.

For example: create a token path called color/timeline/pastDue and rename iOS’s variable pastDueTimelineColor and Android’s date_selection_bar_past_due variable to timelinePastDue.

Tasks:

1 - Map colors currently used in the mobile apps to associated Paragon primitive colors

Output: A map showing current Figma color names, current values, and proposed Paragon primitive analog. A proposal for any color additions to Paragon in the case of colors with no clear Paragon analog (ex: Accent color)

Acceptance Criteria: 1. every Figma color is mapped to a Paragon Primitive. 2. Agreed approach on mobile-unique colors with the Paragon WG.

2 - Visually confirm acceptability of Paragon colors

Output: The Figma file should use the same primitive color names for future alignment with Paragon, however, if the Paragon colors are not acceptable, override the primitive colors in the mobile definitions

Acceptance Criteria: Mobile Design WG acceptance. The colors should not be a regression in design.

Note: If the associated Paragon primitive values are visually unacceptable, we will still use the Paragon token names (Primary-500, Light-300, etc) and swap the values.

3 - Propose semantic token paths and remapping.

Output: A table with each proposed semantic token path, the current iOS/Android variable names it maps from, the proposed unified code facing variable name, and the associated primitive token for light and dark themes.

Acceptance Criteria: 1. every existing variable name in both platforms is mapped to a semantic token path. 2. iOS and Android dev team agreement 3. there is a light and dark mapping for each semantic token. 4. The final designs w/ colors should satisfy WCAG color contrast requirements.

Phase 2: Shared token source and structure for mobile apps

1 - Create shared token files

Output: structured token files added to Paragon: mobile primitive overrides, mobile semantic aliases, and a scaffold for mobile component tokens to be filled in incrementally with Phase 3 progress.

Acceptance Criteria: shared token source file.

Note: at this point, we will not suggest color changes. This will happen when component tokens are generated as we’ll be better suited to check for accessibility at that point when we determine how component tokens are decided.

Phase 3: Refactor Figma file

Refactor the Figma file to utilize semantic tokens defined in Phase 1.3 above. As part of this phase, we will define component tokens and check for accessibility.

1 - Refactor Figma file: Semantic

Output: Figma file updated to use color variables aligned with semantic tokens

Acceptance Criteria: Color definitions should be well defined and no longer reference styles

2 - Define component tokens

Output: Component token variables in Figma. A component definition artifact that shows all defined component tokens and semantic token values. Any proposed changes to semantic tokens because of accessibility issues

Acceptance Criteria: Each component token should be tested in app in both dark/light mode for accessibility.

Phase 4: Mobile app refactors/ Paragon additions

[To confirm color resolution path] Paragon has mobile-primitive overrides and semantic tokens. Style Dictionary outputs platform-specific color files. At run time, any primitive token overrides are handled.

In scope for Phase 4: To Paragon, add mobile primitive override and mobile semantic token definitions file.

Note: need input on best way to stage the refactor from providers/ operators.

Future Phases

Move to unifying other tokens (text styling, spacing, radius).

Risks

This refactoring work will be a significant change to the mobile app code bases. As such, these changes will potentially require a moderate maintenance effort for all forks of the apps.

Appendix: Current Color Reference

Paragon Token Architecture

Paragon uses a 3-tier token structure processed by Style Dictionary. The first tier are the primitive colors that define raw hex values (ie: primary-100). These are built using color mixes (adding white or black) to base colors. The second tier are semantic tokens that are set by aliasing primitive variables. The third tier are component tokens that reference the semantic tokens.

Tier

Name

Location

Contents

Tier

Name

Location

Contents

1

Primitive / Global

tokens/src/themes/light/global/color.json

Raw hex values: white, black, blue, red, green, gray.100-900, primary.100-900, brand.100-900, etc.

1

Core Global (non-color)

tokens/src/core/global/

spacing, typography, elevation, breakpoints, transition

2

Semantic / Alias

tokens/src/themes/light/alias/color.json

Named roles: color.bg.base, color.text, color.border, color.theme.bg.primary, etc. — reference primitives via {color.primary.500}

3

Component (structure)

tokens/src/core/components/

Per-component spacing, typography, sizing (Alert, Button, Card, ~30+ components)

3

Component (color)

tokens/src/themes/light/components/

Per-component color assignments referencing semantic aliases (e.g., alert.bg.success → {color.theme.bg.success})

Current outputs: CSS custom properties only. No mobile outputs exist.
Themes: Only light theme exists.
Build tool: Style Dictionary with @tokens-studio/sd-transforms.
Token reference syntax: {color.primary.500} resolves at build time.

iOS App Color System

iOS does not use primitive colors, but instead hard codes each semantic color and passes that definition to the UI.

iOS defines the raw colors in the Assets.xcassets catalog, but these are semantic values hard-coded, not primitive colors. Each semantic color is hard-coded within the Assets.xcassets catalog and that semantic name is then carried through the generated asset accessor layer (ThemeAssets) and the app-facing theme API layer (Theme.Colors / Theme.UIColors).”

Source of truth: 77 individual .colorset files in Theme/Theme/Assets.xcassets/Colors/
Each file is a Contents.json with light and dark appearance variants. Values are raw RGBA floats or hex; they do not reference Paragon primitives.

Access chain:

.colorset files → SwiftGen (automated Xcode build phase via ${PODS_ROOT}/SwiftGen/bin/swiftgen) → ThemeAssets.swift (generated, type-safe accessors) → Theme.Colors.* (handwritten wrapper in Theme/Theme/Theme.swift — public API) → All app modules

App code uses: Theme.Colors.accentColor, Theme.Colors.background, etc.
SwiftGen config: Theme/swiftgen.yml — reads Theme/Assets.xcassets, outputs Theme/SwiftGen/ThemeAssets.swift

Full list of iOS token names (from ThemeAssets.swift):
accentButtonColor, accentColor, accentXColor, alert, avatarStroke, background, backgroundStroke, cardViewBackground, cardViewStroke, certificateForeground, commentCellBackground, courseCardBackground, courseCardShadow, datesSectionBackground, datesSectionStroke, nextWeekTimelineColor, pastDueTimelineColor, thisWeekTimelineColor, todayTimelineColor, upcomingTimelineColor, primaryHeaderColor, secondaryHeaderColor, courseProgressBG, deleteAccountBG, infoColor, irreversibleAlert, loginBackground, loginNavigationText, primaryButtonTextColor, primaryCardCautionBG, primaryCardCourseUpgradeBG, primaryCardProgressBG, onProgress, progressDone, progressSkip, selectedAndDone, progressLineBG, progressPercentage, circleProgressBG, navigationBarTintColor, secondaryButtonBGColor, secondaryButtonBorderColor, secondaryButtonTextColor, disabledButton, disabledButtonText, styledButtonText, success, textPrimary, textSecondary, textSecondaryDark, textSecondaryLight, textInputBackground, textInputPlaceholderColor, textInputStroke, textInputTextColor, textInputUnfocusedBackground, textInputUnfocusedStroke, toggleSwitchColor, warning, warningText, white, shade, shadowColor, slidingTextColor, slidingSelectedTextColor, slidingStrokeColor, snackbarErrorColor, snackbarInfoColor, snackbarTextColor, snackbarWarningColor, tabbarActiveColor, tabbarBGColor, tabbarInactiveColor, socialAuthColor, resumeButtonBG, resumeButtonText
(77 total)

Actual hex values: only two colorsets were read in detail (AccentColor light: RGBA ≈ #3C68FF, dark: ≈ #5378F8; Background light: #FFFFFF, dark: ≈ #19212E). Full inventory requires reading all 77 files.

Android App Color System

Android has some primitive color variables defined in Colors.kt with some semantic colors also defined alongside. These colors use a different structure than those in Paragon (ie: instead of color scales, Android defines colors like light_primary and light_secondary. The semantic variables are defined in Theme.kt and reference color definitions from Colors.kt. Light and dark mode colors are defined in the same file. AppColors is the analog to iOS’s Theme.Colors / Theme.UIColors.

Source of truth: core/src/openedx/org/openedx/core/ui/theme/Colors.kt
~75 named val declarations with hardcoded hex values (e.g., val light_primary = Color(0xFF3C68FF)).

Access chain:

Colors.kt (raw hex vals, light_* and dark_* prefixed) → AppColors.kt (data class organizing colors into semantic fields) → Theme.kt (creates LightColorPalette / DarkColorPalette, wraps Material3 ColorScheme) → MaterialTheme.appColors (Compose CompositionLocal) → All Compose UI

App code uses: MaterialTheme.appColors.primaryButtonBackground, MaterialTheme.appColors.textPrimary, etc.

colors.xml: Contains 5 values (background, primary, splash, checked_tab_item, unchecked_tab_item). Used for legacy XML-inflated views and splash screen, not the Compose color system.

No hardcoded UI hex in feature modules: Course, dashboard, discovery, auth, profile, discussion modules contain no hardcoded hex color values in Kotlin files.

Current AppColors fields (semantic names, from AppColors.kt):
textPrimary, textPrimaryVariant, textPrimaryLight, textHyperLink, textSecondary, textDark, textAccent, textWarning, textFieldBackground, textFieldBackgroundVariant, textFieldBorder, textFieldText, textFieldHint, primaryButtonBackground, primaryButtonText, primaryButtonBorder, primaryButtonBorderedText, secondaryButtonBackground, secondaryButtonText, secondaryButtonBorder, secondaryButtonBorderedBackground, secondaryButtonBorderedText, cardViewBackground, cardViewBorder, divider, certificateForeground, bottomSheetToggle, warning, info, infoVariant, onWarning, onInfo, rateStars, inactiveButtonBackground, inactiveButtonText, successGreen, successBackground, datesSectionBarPastDue, datesSectionBarToday, datesSectionBarThisWeek, datesSectionBarNextWeek, datesSectionBarUpcoming, authSSOSuccessBackground, authGoogleButtonBackground, authFacebookButtonBackground, authMicrosoftButtonBackground, componentHorizontalProgressCompletedAndSelected, componentHorizontalProgressCompleted, componentHorizontalProgressSelected, componentHorizontalProgressDefault, tabUnselectedBtnBackground, tabUnselectedBtnContent, tabSelectedBtnContent, courseHomeHeaderShade, courseHomeBackBtnBackground, settingsTitleContent, progressBarColor, progressBarBackgroundColor, gradeProgressBarBorder, gradeProgressBarBackground, assignmentCardBorder
(~62 semantic fields + Material3 ColorScheme passthrough)

Architectural Context

What Paragon's 3-tier structure means for mobile

Paragon's component tokens (Tier 3) are scoped to React web components: Alert, Button, DataTable, Dropdown, Form, etc. Mobile apps have no use for these directly.

Mobile's relationship to Paragon's tiers:

  • Tier 1 (Primitives): Mobile and web may eventually draw from the same primitive palette, but for now, these initial efforts should be focused on the semantic/alias lever so we can keep the visual stylings of web and mobile as is.

  • Tier 2 (Semantic/Alias): Partial overlap. Some aliases like color.bg.base, color.text have mobile equivalents. Others are web-specific, and others still are mobile-specific.

  • Tier 3 (Component): Mobile defines its own. PrimaryButtonTextColor (iOS) / primaryButtonText (Android) are mobile's Tier 3 — they reference mobile-specific semantic tokens.

Mobile will always have a superset of Paragon's token vocabulary. Mobile-specific components (CourseDates timeline, Snackbar, Tabbar, etc.) have no Paragon equivalent and will always require mobile-only tokens.

Both apps already have an informal 3-tier structure

Tier

iOS

Android

Tier

iOS

Android

1 (Primitive)

Raw RGBA values in .colorset files

Raw hex in Colors.kt (light_primary = Color(0xFF3C68FF))

2 (Semantic)

Theme.Colors.accentColor, Theme.Colors.background, etc.

AppColors.textPrimary, AppColors.primaryButtonBackground, etc.

3 (Component)

Partially: PrimaryButtonTextColor, CardViewBackground, SnackbarErrorColor

Partially: primaryButtonBackground, cardViewBackground, datesSectionBarToday

The pipeline work formalizes and makes these tiers explicit, without changing the app code that consumes them.