KIQA.DEV
Back to writing
Design
Feb 8, 2026
8 min read
by Kristian Gjergji

How we built a design system for a mobile app with hundreds of screens

When your app has hundreds of components and a small team, having a shared set of rules isn't optional. This is how we did it.

Spindare has hundreds of screens. A feed, a challenge system, a leaderboard, user profiles, settings, onboarding flows, notifications, a reward wheel, a social graph. When we started building it, we didn't have a design system. We had a Figma file and good intentions.

By screen 40, the inconsistencies were already obvious. Buttons in different screens had slightly different padding. Border radii were inconsistent. Spacing was eyeballed. Dark mode had holes. This is how we fixed it, and what we'd do differently from the start.

Why a design system matters more for mobile

On the web, inconsistency is tolerable to a point. Users scroll past it. On mobile, users tap, swipe, and gesture through interfaces that need to feel native and predictable. A button that's 2dp taller on one screen than another is the kind of thing users notice without being able to articulate what's wrong. They just say the app 'feels off'.

With hundreds of screens and a team of two, without a design system we were going to ship something that felt off.

The token layer

We started with a design token file. Not components yet, just values. Every colour, every spacing unit, every font size, every border radius, every shadow defined in one place:

typescript
// tokens.ts
export const colors = {
  primary: '#F97316',
  primaryDark: '#C2410C',
  background: '#0A0603',
  backgroundElevated: '#141009',
  surface: '#1C1510',
  border: 'rgba(255,255,255,0.08)',
  text: {
    primary: 'rgba(255,255,255,0.92)',
    secondary: 'rgba(255,255,255,0.52)',
    muted: 'rgba(255,255,255,0.28)',
  },
} as const;

export const spacing = {
  xs: 4,
  sm: 8,
  md: 12,
  lg: 16,
  xl: 24,
  '2xl': 32,
  '3xl': 48,
} as const;

export const radius = {
  sm: 6,
  md: 12,
  lg: 16,
  xl: 24,
  full: 9999,
} as const;

This single file became the source of truth. Every StyleSheet in the app imports from here. No hardcoded colour hex strings anywhere. No magic numbers.

The component layer

With tokens established, we built primitive components that consumed them. Not full feature components, just the primitives that everything else is built from:

  • Text: with preset variants (h1, h2, body, caption, mono, label)
  • Box: a styled View with token-aware margin, padding, and background props
  • Button: primary, secondary, ghost, and destructive variants with loading and disabled states
  • Avatar: with fallback initials, size variants, and online indicator
  • Card: the surface component, with optional border and shadow
  • Icon: a wrapper around our icon set with consistent sizing

Every feature component is composed from these primitives. A PostCard is a Card containing a Box, Avatar, Text components, and a row of Icon buttons. Nothing in PostCard creates its own padding or colour. It delegates that to the primitives.

Dark mode done right

Spindare is dark-first. We don't support light mode yet. This made the token layer simpler, no semantic colour mapping needed. But we still had to make sure dark mode was systematic rather than 'dark background with white text'.

The key insight: dark UIs need more elevation levels than light ones. In light mode, surfaces are distinguished by hue (grey tints). In dark mode, you distinguish them by lightness, how far above the background a surface sits. We defined four elevation levels: background, surface (slightly raised), elevated (cards, modals), and overlay (bottom sheets, dialogs). Each level is a specific token, not a one-off colour choice.

What we skipped and paid for later

We didn't document the system. We knew what the tokens meant, but new screens added later would occasionally use the wrong token because there was no written guidance on when to use `surface` vs `elevated`. We fixed this eventually with a short internal wiki page, but it should have been written when we created the tokens.

We also didn't build a component preview environment early enough. A Storybook equivalent for React Native (or even just a DevScreen with all the primitives rendered) would have caught visual regressions much earlier. We added one in month three. It caught four bugs immediately.

🎨

The ROI on a design system front-loads the cost. The first week of building the token and primitive layer feels slow. Every week after that, building new screens becomes faster than it would have been without it. If you're building an app with more than 20 screens, start with the token layer on day one.

Written by

Kristian Gjergji

Developer · Kosovo / Italy

Work with me →
Chat on WhatsApp