Live · in active development

GetirFinans Design System

Role
Design System Lead
Timeline
2025 — Present
Team
Solo owner, with iOS/Android engineering and 20 domain designers
Platforms
Web, iOS, Android

GetirFinans started as a product inside the Getir super-app: light mode only, inheriting the parent shell's conventions. The color foundation was a flat variable list named by appearance (GrayPrimary, OffWhite, DarkPurple, PalePurple, FintechGreen). No primitives, no semantics, no mode awareness. Design lived across 20 independent domain Figma files with no component sync protocol. Then leadership asked for dark mode.

Most design system case studies show a system that was never load-bearing. This one was rebuilt underneath a running product, and the constraint that forced it is the same constraint that proved it worked.

What this case study covers, in four deliverables:

  1. 01A two-tier token architecture replacing the legacy color file
  2. 02Dual-mode support across web, iOS, and Android
  3. 03A documentation site the team actually uses
  4. 04A governance model and roadmap the org adopted

Where it started

The Legacy System

The starting color system was a flat list: Getir-Primary, GraySecondary, PalePurple, FintechGreen. Names described visual properties, not intent. No primitive/semantic separation. No mode awareness. It worked well enough for a single-mode app living inside a larger shell.

Hardcoding values and branching on isDark inside every layout file is how design-system debt compounds. Dark mode becomes an audit, styles duplicate, and a brand change touches hundreds of call sites.

// ✕ Legacy — the component owns theme logic
import { useColorScheme } from "@/hooks/useColorScheme";

export function ConfirmButton() {
  const isDark = useColorScheme() === "dark";
  return (
    <button
      style={{
        backgroundColor: isDark ? "#2A2342" : "#5D3EBC",
        color: "#FFFFFF",
        borderColor: isDark ? "#5D3EBC" : "#EAEAEA",
      }}
    >
      Onayla
    </button>
  );
}

Governance debt. A single color change (say, replacing brand purple) means editing every conditional style across the codebase.

The foundation

System Architecture

The foundation is a two-tier token system: primitives carry values, semantics carry intent.

Primitives are raw palette scales: purple-light.50, neutral.900, cool-gray.500. They don't carry meaning, just values.

Semantics follow a {property}/{context}/{variant} pattern: bg/surface/default, text/content/primary, border/input/default. They describe what a color does, not what it looks like.

The token collections are primitives, semantics, and [legacy]. The legacy collection is kept deliberately during migration: it lets 20 domains migrate incrementally instead of in one synchronized big-bang refactor.

Modes (light and dark) are resolved at the semantic layer. A semantic token's identity is stable; only its primitive binding changes per mode. The system spans three platforms: web, iOS, and Android.

Token Formula Builder
{category}/{concept}/{role}

Combine a category, concept, and role to build a semantic token, then inspect how it resolves in each mode.

Category
Concept
Role
bg/surface/defaultDefault page background and main card surfaces
Light
#FFFFFF
neutral.white
Dark
#121212
neutral.900

Namespace isolation

bg/ai/*, text/ai/*, and icon/ai/* form an isolated namespace for AI surfaces. The rationale is architectural, not aesthetic. AI surfaces were sharing semantic tokens with error and success states. In light mode the collision was invisible. In dark mode it broke.

The dimensional layer, honestly stated

Spacing (9 steps: 0/4/8/12/16/24/32/48/64), size (10 steps, including a 2-step touch target scale), radius (8 steps), and breakpoint (3 steps) primitives are built. Semantics exist for color only; the dimensional primitives haven't been promoted into the semantic collection yet.

This is a deliberate sequencing decision: color was the layer under production pressure and the layer where a mistake is visible to every user. Ship the semantic layer where it's load-bearing, build primitives beneath the rest, and promote them once the naming has been proven by the color layer. The color system is the template: same two-tier shape, same slash-delimited naming.

Three decisions define this architecture

Brand invariance: bg/action/primary resolves to #5D3EBC in both modes. Brand is a mode-independent anchor. Most fintech dark modes oversaturate the brand color into large fills; here purple lives at the stroke, accent, and border layer.

Brand as surface

Vadeli hesap

₺100.000

Devam et

bg/action/primary as surface

The same token used as a large fill. Legible, and it makes every screen a brand screen.

Brand as anchor

Vadeli hesap

₺100.000

Devam et

bg/action/primary as action + border

Neutral surface, purple reserved for the action and the border it owns.

Same token, same hex, both modes. The decision was never which purple, it was how much surface purple is allowed to own. Restraint here is what keeps the accent legible when it matters.

Status colors range-switch, they do not invert. green.50 in light mode becomes green.900 in dark mode: same semantic role, different scale position.

Positional equivalence

Transfer completegreen.50
Card declinedred.50
Limit almost reachedorange.50

What “give us dark mode equivalents” produces: the light scale positions, unchanged, on the dark canvas.

Range switch

Transfer completegreen.900
Card declinedred.900
Limit almost reachedorange.900

Same semantic roles, resolved into the 900s. Contained instead of glowing.

Both panes are the same three semantic tokens. Only the primitive they bind to changed. The left one is what a 1:1 mapping gives you, and it is why the brief had to be argued with before anything could be designed.

Keeping the legacy collection alive: deleting it would have forced a synchronized migration across 20 domains. Keeping it made migration incremental and reversible.

Live Component Specs
button + input · semantic states
Button
bg/action/primary#5D3EBC
text/action/inverse#FFFFFF
Input
bg/input/default#FFFFFF
border/input/*#EAEAEA

Chapter one

Dark Mode as the Forcing Function

The brief from product and engineering was direct: "give us dark mode equivalents of our colors." That framing is wrong. Dark mode is not an inversion of light mode. A 1:1 hex mapping breaks status colors, collapses elevation hierarchy, and produces surfaces that are technically darker and perceptually wrong. Resolving the framing before designing anything was the first real design decision.

The dark palette: canvas #0E0E0E, near-black with no blue bias and not pure black. Surfaces at #1A1A1A and #262626 in tight steps: flat and dense rather than heavily elevated. Status colors sit in the 900 range, muted, with no neon-on-black. Reference points: Apple HIG as primary, Revolut and iOS system apps for comparison.

Engineering's initial resistance was correct on its own terms. Refactoring 20 domains is expensive and the brief asked for hex values. What changed it was showing them the token system from their side: fewer conditionals, one source of truth per semantic role, cleaner handoff. They became advocates.

Boundary conditions

Where the Token System Ran Out

These are boundary conditions of a token architecture, not failures.

FX line chart: the SVG stroke was hardcoded in markup with a Display P3 fallback. The fill gradient required a conditional SwiftUI render path (an isDark environment check, separate gradient values per mode), not a token swap. The root cause: the token system is single-value. Data visualization needs ordered palettes, contrast guarantees, and multi-series logic. Colors designed for UI surfaces were reused for data. Charts were never modeled as a semantic domain.

Three more edges, each outside what tokens can reach:

  • Google Maps runs on a separate JSON style file the token system doesn't reach. Purple was tried on base tile geometry and explicitly removed; the final version is neutral geometry with purple reserved for overlays and markers.
  • Lottie color management sits outside the token system entirely: parallel animation files or layer-level overrides. Android constraint: layers nested inside precomps don't support runtime color override.
  • Liquid glass on the home action grid was evaluated and rejected: contrast failure in dark mode, and it doesn't satisfy HIG intent for static containers.
FX Exchange Chart
conditional render · SwiftUI / SVG path
Dolar / Türk LirasıUSD/TRY 32.8450
+0.34%
09:0011:0013:0015:0017:00
stroke#5D3EBC · hardcoded in markup
fillconditional gradient · not a token swap

bg/ai/primary in dark mode is #2B2438, a desaturated dark purple, not an inversion of the light value. Dark AI surfaces needed to feel contained and premium, not just dark.

AI Component Spec
isolated namespace · bg/ai/*
Podcast Önerisi24 dk
Yatırım Dünyasında Yeni Eğilimler

Yapay zekânın portföy yönetimindeki yeri ve geleceği.

AI
Akıllı Asistan
Kira ödemeniz için ₺12.500,00 transfer talimatı oluşturulsun mu?
bg/ai/primary#2B2438
bg/ai/border#3B324D

What it exposed

The Governance Failure Dark Mode Exposed

Core UI icons were exported as PNGs per domain and uploaded to separate Directus CDN buckets. No versioning, no registry, no token injection possible at runtime. Dark mode required a full parallel set of PNGs: the same icon existed across 6+ CDN locations, each needing a manual re-export.

A Slack tracking list (name, domain, CDN link, type, request owner, status) was the operational workaround. It documented the absence of a system, and that documentation became the business case for building one.

Evidence, not claims

Documentation as an Artifact of the System

gf-design-system.vercel.app is a Next.js documentation and education site, live and in active use by the team. The color section alone is 7 pages: introduction, primitives, semantics, reality, dark mode, overlay, and gradients.

The "reality" page documents the gap between the system as designed and the system as implemented in production right now. Most design systems hide this. Publishing it is what made the migration tractable.

The repo split (design system source and documentation, previously one monorepo) was a governance decision, not a technical one. The docs site consumes published token packages, so it cannot describe a token that doesn't ship.

Beyond the site: facilitated token usage sessions across the design team, wrote implementation guidelines, and educated designers on the two-tier model.

gf-design-system.vercel.app/color/semantics
Color / Semantics

Semantics

{property}/{context}/{variant}. bg/surface/default, text/content/primary, border/input/default — what a color does, not how it looks.

#FFFFFF
#FAFAFA
#0E0E0E
#757575
#EAEAEA

Built on the system

Proof

The new bottom sheet, designed after the token system shipped, is the clearest proof of what the system enables. Every color decision is a semantic token reference: no hardcoded values, no one-off overrides. Light and dark are one design file with a mode toggle. That is the difference between adapting a product to dark mode and building a product that understands it.

Standalone App
rating sheet · every color a semantic token
09:41
5G100%

Transfer İşlemi Başarılı

₺2.500,00 Ulaş Alyeşil hesabına gönderildi.

GetirFinans'ı seviyor musun?

Dürüst görüşün bizi daha iyi yapar.

Counted

Results

The foundation was replaced underneath a running product without a freeze, a big-bang refactor, or a rollback.

Full migration
2 months
Every sub-product, with the core dev team, ahead of the standalone app release
Domains migrated
20
All of them, not a partial rollout
Platforms
3
Web, iOS, Android
Modes
2
Resolved at the semantic layer
Token collections
3
Primitives, semantics, legacy
Dimensional primitives
30
9 spacing, 10 size, 8 radius, 3 breakpoint

Shipped

  • The token system is live in production across the getirfinans app, on all three platforms.
  • The legacy color file is fully replaced. Accounts, loans, FX and cards all resolve through the semantic layer.
  • The migration landed ahead of the standalone app release, which was the deadline that mattered.
  • gf-design-system.vercel.app is live and in active use by design and engineering.

Adoption

  • Every sub-product is on the semantic layer. There is no domain still reading raw hex.
  • The design team was trained on the two-tier model through facilitated token sessions.
  • Engineering went from resisting the refactor to advocating for it, and now files token requests rather than hardcoding around them.

What it changed beyond the system

  • getirfinans.com was redesigned end to end on the new visual language.
  • A formal token governance process exists where there was none.
  • The SVG icon migration was approved and prioritized, which is the fix for the CDN sprawl dark mode exposed.
  • The monorepo split into a source repo and a documentation repo, so the docs cannot describe a token that does not ship.
  • Design system deep dives now run after each sprint.

Roadmap

What's Next

  1. 01Semantic dimensional layer: promote spacing, radius, and size primitives into semantics using the existing names. Don't redesign the ramp. Publish.
  2. 02Component tokens: no component tier exists today. It's the layer above semantics and it isn't built yet.
  3. 03Icon migration: PNG to SVG, killing CDN bucket sprawl and unlocking runtime theming.
  4. 04Data visualization tokens: charts need a modeled semantic domain (ordered palettes, contrast guarantees, multi-series logic). The FX chart is the open ticket.
  5. 05Component library: downstream of all of the above. It doesn't start before the foundation carries weight.

Retrospective

Reflection

What worked

Sequencing color first: the layer under production pressure, the layer where errors are visible to every user, and the naming template for everything after it. Publishing the gap: the reality page and the Slack tracking list both documented the distance between design and implementation, and making the debt visible got it funded. Isolating the AI namespace early rather than redefining shared tokens under pressure. Bringing engineering in by showing them the system from their side.

What I'd do differently

Audit color usage in context before designing anything. Elevation problems surfaced during implementation instead of before it; a screen-by-screen audit of how colors were actually used, not how they were named, would have caught them. Model charts as a semantic domain from the start rather than reusing surface colors for data. Finish the dimensional semantic layer alongside color rather than leaving primitives promoted but unmapped. The gap is defensible. It was still avoidable.

I rebuilt the color foundation of a live fintech product, solo, while convincing the team that the problem was never dark mode. It was the system underneath.