Design systems for small app teams: start smaller than you think

How a small app team can build a design system that lasts: tokens first, a dozen components, notes in the design file, clear names and a first month plan.

Design systems for small app teams: start smaller than you think

Ask a small app team to search its codebase for hard-coded colors and you get a familiar silence. The brand blue turns up in four slightly different hex values, there are more grays than anyone can explain, and two screens use a red that exists nowhere in the design file. Nobody did anything wrong. Each value was a reasonable choice on the day someone made it.

That drift is what a design system fixes, and a small team doesn't need the version they've seen from large companies, with a public website, hundreds of pages and a team of its own. It needs a shared vocabulary small enough to stay true: a set of tokens, about a dozen components, notes where people already look, a naming rule and a way to change things without breaking them. Everything else can wait until there's a reason for it.

Start with tokens, not components

Tokens are named design decisions: a color, a type style, a spacing step. Components are built from them, so getting the tokens right first means every later component inherits consistent values for free.

Color in two layers

Keep two layers of color. The first is a primitive palette, the raw values with neutral names such as blue-500 or gray-100. The second is a set of semantic roles that describe a job: text.primary, text.secondary, surface.raised, action.primary, status.danger. Screens and components use only the semantic layer. Dark mode then becomes a second set of values for the same semantic names rather than a parallel design.

Both platforms already think this way. iOS has semantic system colors such as label and systemBackground that adapt to appearance and accessibility settings. Material 3 describes its design tokens in tiers, from reference tokens holding raw values to system tokens such as md.sys.color.primary that assign roles, and component tokens on top. Borrow the structure even if you don't use Material.

The semantic layer is also the right place to check contrast. If text.secondary on surface.raised passes in both light and dark mode, every screen that uses that pair passes too. The thresholds are the ones in my mobile accessibility checklist: 4.5:1 for body text, 3:1 for large text and the parts of controls people need to see.

Type by role

Define six to eight text styles named by what they do (title, headline, body, callout, caption) rather than by size. A name like text-17 stops being true the moment someone changes the size. Map each style to the platform's scaling: on iOS, relate custom fonts to a system text style so Dynamic Type still works; on Android, set sizes in sp. A type scale that ignores the user's text size setting is a bug, not a style choice.

Spacing, radius and elevation

Pick a base unit of 4 or 8 points and a short scale on top of it. Five to seven steps is plenty. The same goes for corner radius (two or three values) and elevation or shadow (two or three levels). Fewer values make each choice quicker and make inconsistencies visible in review.

enum Space {
    static let xs: CGFloat = 4
    static let s: CGFloat = 8
    static let m: CGFloat = 16
    static let l: CGFloat = 24
    static let xl: CGFloat = 32
}

A dozen components, not fifty

A component earns a place in the system the third time it's needed, not the first. Until then it lives in the feature that uses it. This one rule keeps a small library from filling up with one-off banners that nobody reuses.

Where the platform already provides a component, wrap it rather than redraw it. When Apple shipped iOS 26 with its Liquid Glass design in September 2025, apps rebuilt with the new SDK picked up the new material in standard bars and controls, while custom-drawn versions kept whatever someone had hard-coded. A thin wrapper over the system tab bar costs little and follows the platform when it changes.

Component Start with Leave for later
Button Primary, secondary, text; normal and disabled Icon plus label, loading state
Icon button One size, with an accessible label built in Toggle variant
Text field Label, helper text, error Prefix and suffix, character counter
List row Title, subtitle, trailing chevron or switch Swipe actions
Card One padding, one radius, one elevation Media variants
Top bar System bar wrapped, title and actions Large and collapsing variants
Tab bar System component wrapped Badges
Sheet System sheet with standard handle and actions Custom heights
Dialog Title, message, two actions Input dialogs
Snackbar or toast Message plus one action Queueing rules
Empty state Line of text, one action, optional image Illustrated variants
Loading Spinner and a skeleton for list rows Shimmer effects

The empty state is on that list on purpose. It's the screen new users see first, and I made the case for treating it as teaching space in onboarding flows people actually finish.

Documentation where people already look

Small teams don't read documentation sites; they read the design file and the code. Put usage notes next to each component in the design file, a few lines on when to use it and when not to, and keep a short page of decisions, such as "only one primary button per screen" or "destructive actions always ask for confirmation in a dialog". Most design tools, Figma included, let you attach a description to a component that people see when they pick it from the library.

In code, the best documentation is a preview that shows every variant: a SwiftUI #Preview or a Compose @Preview per state. Those previews are also natural targets for snapshot tests, which catch the change that quietly moves every button two points to the left; Tomasz covered the setup in his snapshot testing field guide.

Naming that survives

Names outlive values, so spend more time on them than on the colors. A few rules I hold to:

  • Name by role, not appearance: status.danger, never red. The brand might turn orange.
  • Use the same names in the design file and in code. If the designer says "surface raised" and the developer searches for surfaceRaised, they should land on the same thing.
  • Write down one separator convention per context, such as dots in the design file and camelCase in Swift and Kotlin, and never mix them.
  • Never name a component after the screen it first appeared on. CheckoutButton will end up on the profile screen.

If you export tokens from a design tool into code, look at the format the Design Tokens Community Group published as its first stable version, 2025.10, on 28 October 2025. It's a W3C Community Group report rather than a formal W3C standard, but it gives you a vendor-neutral JSON structure, with $value and $type properties and aliases written in curly braces, which more and more tools can read. The format specification is long; the sections on groups and aliases are the ones a small team needs first.

Versioning without ceremony

Use semantic versioning in plain terms. A patch release fixes a value. A minor release adds a token or a component. A major release renames or removes something, which means work for everyone using it. Keep a changelog with a few lines per release, and when you rename a token, keep the old name as an alias for one release so screens can move over gradually.

Version the tokens and the code components together, tag the release in the repository, and publish the design library with a note that matches the changelog. If the version in the design file and the version in the app ever disagree, believe the app and fix the file.

When to formalize

For a team of three to ten, a token file, a library page in the design file and a dozen components are enough for a long time. The signs that you need more structure are specific: a second app or a second platform team starts using the same components, someone joins and takes weeks to learn which pieces are current, a rebrand is coming, or the same question about which gray to use comes up for the third time in a month. Even then, add an owner and a simple way to propose changes before you add a documentation site.

The first month

  1. Week one: screenshot every screen in the app and collect every color, font size, spacing value and corner radius in use. Count them. The count is your baseline.
  2. Week one: define the primitive palette, the semantic color roles, the type scale and the spacing scale. Check contrast for every text and surface pair in both appearances.
  3. Week two: wire the tokens into code on each platform and convert one real screen to use only tokens. Fix whatever that screen reveals about the token set.
  4. Weeks two and three: build the first six components from the table above, in the design file and in code, with the same names and the same variants.
  5. Week three: write the usage notes, add previews for every variant, and set up snapshot tests for them.
  6. Week four: call it version 1.0.0, write the first changelog, and walk the team through it in half an hour. Agree on one place where changes are proposed and one person who decides.
  7. End of week four: choose the next six components based on what the next feature on the roadmap needs, not on what a complete library would contain.

More pieces on interface details and team practice live in the app design track. If you start anywhere, start with that search for hard-coded hex values. Write the number on a sticky note, and watch it go down as the first screens move to tokens.