-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Type of Request
Product Proposal (larger features)
Feature Description
Overview
This proposal describes a tokens-first theming pipeline for Open edX Mobile, where Paragon design tokens are the single design source of truth, and mobile apps adjust their theme at runtime without requiring new builds.
Design tokens are defined in Paragon as JSON and then compiled via Style Dictionary into:
- CSS artifacts for the web (as today, unchanged), and
- a single mobile artifact
colors.json, consumed by both Android and iOS.
colors.json is deployed as a static file (for example, in the LMS or on a CDN) and exposed via a stable URL.
Live demo:
https://stepanok.com/test-themes/
Detailed Product Proposal
https://openedx.atlassian.net/wiki/x/MoDrPQE
Mobile apps:
- do not parse “raw” Paragon JSON directly;
- do not rely on platform-specific artifacts such as
colors.xmlor Swift/ObjC theme files; - instead, fetch a single
colors.jsonfrom a stable URL; - build the light theme directly from
colors.json; - locally derive a dark theme using deterministic color math (no dedicated dark palette required in tokens);
- keep neutral gray surfaces under app control so the UI remains clean and legible even if operators choose extreme brand colors.
If tokens are unavailable or the feature is disabled, the app falls back to its built-in color configuration.
The feature is controlled by:
DESIGN_TOKENS_ENABLED: truein config.yaml.
This approach is also a step toward a future multi-tenant mobile application: a tokens-first pipeline and colors.json make it possible to swap themes for different tenants without changing the binary.
Problem
Operators occasionally update their branding and color palettes. Even if this doesn’t happen frequently, any such change on mobile today typically means:
- colors are hard-coded into the app;
- changing the palette requires a new release to the app stores;
- color behavior is often out of sync with the web and behaves unpredictably in dark mode.
This causes:
- drift between web and mobile (different colors for the same brand),
- inconsistent or broken dark mode,
- accessibility regressions (insufficient contrast),
- unnecessary operational overhead (a new build even for a once-a-year rebrand).
Additionally, the Open edX ecosystem is moving toward multi-tenant scenarios: one mobile client, multiple installations/tenants. Without a unified token pipeline, this is difficult and expensive to support, because each themed variant tends to turn into a separate branch or build.
We need a single, robust token system that:
- drives both web and mobile from the same token set;
- lets operators change brand colors without rebuilding the app;
- keeps dark mode and contrast under control on mobile;
- does not require operators to maintain a separate dark palette in tokens;
- lays the foundation for a future multi-tenant mobile app, where different operators can receive their own themes on top of a single codebase.
Use Cases
Learner Stories
Consistent brand & readability
As a learner, I want colors to look consistent and readable across light and dark modes, so the app feels trustworthy and comfortable to use.
Operator Stories
Rebrand without a new build
As an operator, I want my mobile app to automatically follow the same design as my website, without manually adjusting colors on every platform. I want a single source of truth for the brand.
Compliance & Governance
Store-safe remote configuration
As a compliance owner, I need theming to be driven purely by static presentation data (tokens) that:
- are fetched securely over HTTPS,
- are applied in a deterministic way,
- do not contain arbitrary code or business logic.
Proposed Solution
1. Single Source of Truth: Paragon Design Tokens
- Design tokens (colors and related metadata) are defined in Paragon as JSON.
- These tokens are the single design source of truth for:
- the web (CSS), and
- mobile apps (via a single
colors.jsonartifact).
Paragon tokens remain “raw” design data. For use in products, they are transformed into consumption-friendly artifacts.
2. Style Dictionary as the Build Pipeline
We use Style Dictionary to compile raw Paragon tokens into:
- Web: existing CSS outputs (no change to how the web works today).
- Mobile: a normalized mobile artifact
colors.jsonthat contains:- brand accent ramps (primary / secondary),
- semantic roles for accent usage (buttons, toggles, links, borders),
- on-colors (text and icons on accent surfaces).
Base pipeline:
Paragon JSON Tokens (design source of truth)
→ Style Dictionary (extended config: web + mobile)
→ Artifacts:
- Web: CSS variables
- Mobile:
colors.json(the contract for Android/iOS)
→ Deployed as static assets (LMS / CDN)
→ Mobile apps fetch colors.json and map tokens into their native theme systems (Compose, SwiftUI, etc.).
3. Distribution & Hosting
The mobile artifact colors.json is deployed as a static file, for example:
https://{lms-address}/static/tokens/mobile/colors.json
Key properties:
- a fixed, documented URL for
colors.json; - can be hosted on:
- LMS static assets,
- an external CDN,
- any operator-controlled static hosting with HTTPS;
- no server-side logic required — just static file hosting.
This matches the expectation that mobile clients can consume themes from “any HTTPS URL,” while keeping the path predictable and documented.
4. Mobile Consumption & Caching
For each mobile app (Android/iOS):
- The app knows the canonical URL for
colors.json. - On cold start, when
DESIGN_TOKENS_ENABLED: true:- the app loads the “last known good theme” from local cache or uses the built-in theme;
- it renders the first frame using this theme (no flash of unthemed UI);
- in the background, it fetches the latest
colors.jsonusing standard HTTP caching (ETag / Last-Modified).
The app validates the received colors.json:
- checks structure and types,
- checks presence of required keys,
- optionally checks signatures/integrity.
If the payload is valid and different from the cached version:
- it is stored as the new “last known good” theme;
- it will take effect on the next cold start.
If the payload is unavailable or invalid:
- the app keeps the current theme (cached or built-in),
- and logs the failure for later analysis.
This ensures:
- deterministic theming behavior;
- no mid-session “color popping”;
- any issues with
colors.jsonresult in a safe fallback to a stable theme.
5. Dark Mode: Local Derivation from Light Tokens
The design tokens do not provide a dedicated dark palette.
- Operators define only a light palette:
- base brand color,
- accent ramps,
- semantic roles (primary/secondary, on-colors, borders).
The mobile app uses deterministic color math to derive the dark theme from these light tokens.
When computing the dark theme, we consider:
- target contrast levels (WCAG),
- brightness caps (to avoid glare in dark mode),
- saturation limits (to avoid “neon” colors).
Behavior:
- Light theme: colors from tokens are applied as-is, with no automatic adjustment.
- Dark theme: is computed on-device using the formulas below, with the light tokens as input.
As a result:
- dark mode logic is controlled by the mobile apps;
- readability and visual quality are preserved even with suboptimal brand colors;
- operators do not need to maintain two independent palettes (light/dark).
6. Neutral Surfaces & Gray Backgrounds
To keep the UI coherent and readable, neutral gray surfaces are not controlled by tokens in v1:
- The app defines and owns its own set of neutral surfaces:
- background colors,
- cards,
- list surfaces,
- base layout layers.
These surfaces:
- are neutral in color,
- are tested against a wide range of brand accents,
- are optimized for accessibility and readability.
The role of tokens in v1 is to:
- control accent colors (primary/secondary),
- control text/icon colors on accent elements,
- control accent borders on top of neutral surfaces.
Consequences:
- even with very bright, dark, or unusual brand colors, the UI remains “designed”:
- core surfaces stay neutral,
- accents are automatically adjusted as needed;
- operators get strong brand presence through buttons, toggles, and highlights, without risking unreadable backgrounds.
In future versions (v2+), we may allow tokens to drive neutral surfaces as well, but in v1 this is a deliberate app-owned safety layer.
Key Principles
- Single design source of truth: Paragon JSON tokens.
- Dedicated mobile artifact: Style Dictionary generates
colors.jsonas the contract for Android and iOS. - Runtime theming: mobile apps do not depend on build-time theme generation; all color configuration comes from
colors.jsonat runtime. - Local dark mode: the dark theme is computed on-device from light tokens using predictable, testable formulas.
- Stable neutral surfaces: gray backgrounds and base surfaces are owned by the app and are not overridden by tokens in v1.
- Apply theme before first frame: users do not see a flash of the default theme.
- Deterministic color math: color transformations are predictable and debuggable.
- No explicit versioning contracts: if
colors.jsonno longer matches what the client expects, the app simply falls back to its built-in theme. - Foundation for multi-tenant: a runtime token pipeline enables future per-tenant themes without changing the binary.
In Scope (v1)
In scope for v1:
- Colors only, including:
- brand accent ramps (primary),
- semantic roles for accent usage (buttons, toggles, links, borders),
- on-colors for text and icons on accent surfaces.
- Dark theme, computed locally on-device from light tokens.
- Fixed neutral surfaces, defined by the app itself (light and dark).
- Style Dictionary integration:
- extend existing Paragon configuration to generate
colors.jsonfor mobile.
- extend existing Paragon configuration to generate
- Fetch + cache + validation:
- loading
colors.jsonover HTTPS, - using HTTP caching (ETag/Last-Modified),
- structural validation,
- persisting the “last known good theme.”
- loading
Feature flag:
DESIGN_TOKENS_ENABLED: true- when
true, the app attempts to loadcolors.json; - when
false, it uses the built-in theme.
Out of Scope (v1)
Explicitly out of scope for v1:
- typography tokens (fonts, sizes, weights);
- spacing, radius, elevation tokens;
- per-user theming;
- full multi-tenant theming (separate tokens per operator/tenant — that is a future step);
- controlling neutral gray surfaces via tokens.
Implementation Plan
Milestone 1 — Core Token Pipeline & Mobile Integration
Paragon / Style Dictionary
- Define and lock the set of color tokens in Paragon required for mobile.
- Extend Style Dictionary configuration to generate:
- CSS outputs for the web (as today),
colors.jsonfor mobile.
Static Hosting
-
Define the canonical URL for
colors.json, for example:/static/tokens/mobile/colors.json -
Document hosting requirements for operators (LMS static assets or CDN).
Mobile Apps (Light Theme)
Implement:
colors.jsonfetch,- caching and HTTP header usage,
- structural validation,
- fallback to built-in theme.
Wire token-driven accents into:
- buttons,
- toggles,
- primary CTAs,
- links,
- accent borders.
Milestone 2 — Dark Mode & Accessibility
Local Dark Theme Generation
- Implement deterministic dark theme generation from light tokens according to the color math described below.
- Ensure:
- target contrast levels (especially for text),
- brightness caps (anti-glare),
- saturation control (anti-neon).
Neutral Surfaces
- Lock in neutral gray palettes for light and dark modes.
- Test behavior of the UI with a wide range of brand colors (including “difficult” cases).
Accessibility & QA
Add automated checks for:
- contrast of tokenized surfaces,
- visual regressions when brand colors change,
- correct fallback behavior.
Milestone 3 — Operational Polish & Extensions (Optional)
Operator Tools
- CLI or CI validator for
colors.json. - Documentation with examples:
colors.jsonstructure,- typical brand configurations.
Telemetry
Track:
- how often
colors.jsonis used vs the built-in theme, - frequency of fetch/validation errors,
- how often automatic contrast/saturation adjustments are applied.
Future Extensions
- OKLCH-based ramps,
- typography and spacing tokens,
- full multi-tenant theming, where
colors.jsoncan differ per tenant.
Long-Term Ownership & Maintenance
- Engineering / Tech: AXIM Mobile WG + Open edX Mobile maintainers own the implementation and maintenance of the mobile side and its integration with
colors.json. - Design System WG: owns Paragon tokens, ramp structure, and alignment between web and mobile.
- Accessibility WG: formalizes contrast requirements and color math constraints, audits dark-mode behavior.
- Operators: own hosting of
colors.jsonand the choice of brand colors within recommended ranges.
Security & Privacy
- All token artifacts, including
colors.json, are fetched over HTTPS. colors.jsoncontains no PII — only presentation data (colors and metadata).- Optionally, we may use:
- response signing,
- domain or certificate pinning for token URLs.
Cache integrity is ensured through:
- structural validation,
- type checking,
- robust error handling.
On any failure, the app falls back to the last known good theme or its built-in theme.
Error Handling & Edge Cases
-
DESIGN_TOKENS_ENABLED = false
→ The app uses its built-in color configuration (light + dark) and app-owned neutral surfaces. -
Network failure / unreachable URL
→ The app uses the cached “last known good” theme or the built-in theme, and logs the error. -
Invalid
colors.json(schema mismatch, missing keys, invalid values)
→ The payload is rejected, the event is logged, and the previous working or built-in theme remains in use. -
Extreme brand colors (too bright, too dark, too saturated)
→ Local color math:- clamps brightness and saturation,
- enforces minimum contrast,
- maintains visual balance against neutral surfaces.
Telemetry & Success Criteria
Functional
- ≥ 99.9% of cold starts apply a theme before the first frame.
- < 0.1% of sessions hit a token-related fallback.
- ≥ 99% of tokenized surfaces meet target contrast thresholds.
Business
- ≥ 80% reduction in time-to-rebrand for mobile apps.
- ≥ 60% fewer support tickets related to theming and dark-mode issues.
Technical / Reliability
colors.jsonpayload size < 10 KB.- High HTTP cache hit rate.
- p95 fetch time low enough for a smooth UX.
- Logging includes:
- validation results,
- fallback reasons,
- automatic color adjustment counts.
Color Math (Deterministic Formulas)
Note: the implementation may be encapsulated behind utility functions, but behavior must remain deterministic and testable.
Notation (sRGB)
Channels in [0…1].
Gamma Transform (sRGB ↔ linear)
Gamma expand (sRGB → linear):
Gamma compress (linear → sRGB):
Linear mix (per channel in linear sRGB):
Relative Luminance (WCAG)
(R, G, B — in linear space after gamma expansion.)
Contrast ratio:
Targets:
- Body text:
CR ≥ 4.5:1 - Large / semibold text:
CR ≥ 3:1
1) Required Luminance from Target Contrast (Dark-Mode Core)
Given background luminance L_bg (typically 0.01–0.03 for dark UIs) and a light foreground on dark:
We clamp foreground luminance:
- lower bound:
L ≥ L_req(readability), - upper bound:
L ≤ L_cap(anti-glare, e.g., 0.85).
2) Saturation Control (Anti-Neon / Anti-Washed-Out)
Working in HSL: HSL(h, s₀, l₀).
- Initial clamp:
s₁ = min(s₀, s_max), for examples_max = 0.80. - For dark mode (or very low luminance):
- if
L_target < 0.40, thens₁ *= desaturateDarkFactor(e.g.,0.60).
- if
- Lower bound: if
s₁ < s_min, thens₁ = s_min(e.g.,0.40). - Hue stays:
h' = h.
This keeps colors vibrant but controlled, avoiding neon in dark mode and overly pale accents.
3) Lightness Search to Hit Target Luminance
HSL lightness does not map linearly to WCAG luminance, so we solve for l via binary search:
- Start with
HSL(h’, s₁, l). - Convert to sRGB, gamma expand, compute luminance.
- Adjust
luntilluminance ≈ L_target(≈ 20 iterations). - Convert back to sRGB with gamma compression.
Result:
- the color passes the contrast requirement
L ≥ L_req, - luminance is capped
L ≤ L_cap, - saturation stays within
[s_min, s_max].
4) Brand Ramp from Base Color
For a base accent color C₀ (sRGB):
-
Lighten toward white:
$$C_{\text{light}}(t) = \mathrm{mix\_lin}(C_0, (1,1,1), t)$$ -
Darken toward black:
$$C_{\text{dark}}(t) = \mathrm{mix\_lin}(C_0, (0,0,0), t)$$
Recommended t ranges:
- fills:
t = 0.90–0.96, - borders:
t = 0.60–0.75, - pressed/focus:
t = 0.25–0.40(darkening), - “500” ≈ base token.
5) Contrast Checks (Universal)
For any text/icons over surfaces or accents:
- Compute
L_bgfrom the surface. - Compute
L_reqfrom the target contrast. - Adjust the foreground via the saturation and lightness routines above.
For text on accent surfaces:
- Try white first.
- If
CR(white, accent) < 4.5:- raise text luminance up to
0.98, or - slightly darken the accent to meet contrast.
- raise text luminance up to
6) Disabled
For active color C and surface Surface:
-
Disabled color:
$$C_{\text{disabled}} = \mathrm{mix\_lin}(C, \mathrm{Surface}, 0.62)$$ -
Overlays:
α_disabled ≈ 0.38.
7) Elevation Overlays (Dark Mode)
For base surface Base and elevation level e (0, 1, 2…):
Elevated surface:
This adds subtle brightness with elevation while preserving dark-mode character.
8) Borders
-
Light theme:
border = darken(surface, t = 0.06–0.10). -
Dark theme:
border = lighten(surface, t = 0.10–0.16). -
Accent borders (for example, secondary buttons):
- use ramp values around “400” (light) / “600” (dark),
- should maintain
CR ≥ 3:1vs the surface.
Feature Toggle & Deployment
DESIGN_TOKENS_ENABLED: trueWhen enabled:
- mobile apps fetch
colors.jsonfrom the canonical URL; - validate, cache, and apply it on cold start;
- derive the dark theme locally from light tokens;
- keep neutral surfaces under app control.
When disabled, or whenever there is any issue with colors.json:
- the app uses its built-in light and dark themes and its own neutral surfaces.
Open Questions
Where should the dark theme be generated?
This proposal currently derives the dark theme on-device from light tokens. We may instead generate the dark palette server-side (in the Paragon / Style Dictionary pipeline) and expose both light and dark tokens in colors.json, which would simplify mobile implementations (no duplicated color math on iOS/Android) and enable future reuse of the same dark theme on the LMS side.
Implementation Plan (Funding)
Raccoon Gang is currently looking for funding for this proposal.
Link to Product Proposal
https://openedx.atlassian.net/wiki/x/MoDrPQE
Status
New
Proposed By
Ivan Stepanok
Metadata
Metadata
Assignees
Labels
Type
Projects
Status