# App design handoff to developers: what a frame cannot tell them

> A frame is one picture at one size, one density and one text scale, and a phone breaks all four. A useful handoff states rules for what changes (units, scaling text, system insets, motion, states) and uses Dev Mode statuses to show what is final.

By LAIN, 12 Sep 2026. 12 min read. Category: App design. Canonical: https://lainagency.com/resources/app-design-handoff-to-developers

Most handoff advice says "name your layers, design every state, export your assets". That is all true, and every developer has heard it. The harder problem is that a frame in Figma is **one picture at one screen size, one pixel density and one text size**, and a phone changes all four. A handoff that lists measurements for that one picture will be built correctly and still look wrong on the second device. This guide covers the things a frame cannot tell a developer, and how to write them down.

## Why a measured frame is not a specification
A designer drags a gap to 4, a developer reads "4", and the build is "correct". Then the same gap looks different on three phones. Developers fill those holes with judgement. Sizzy's write-up of handoff problems puts it this way: the costly bugs come from undocumented edge cases, not from the happy path everyone reviewed.

Five things change between the frame and the device:

| The frame has | The device does | Hand over |
|---|---|---|
| One pixel density | Fractional densities, so some values land between pixels | The grid unit and a rounding rule |
| One font size | The user scales text up to 3.1 times on iOS | Which layouts must grow, wrap or scroll |
| A clean top and bottom edge | The system draws bars, a Dynamic Island, a home indicator | Which layers go under system areas |
| A prototype with an easing curve | Native springs with their own parameters | Spring values, or the platform's named motion token |
| A file that keeps changing | A build made from yesterday's copy | A visible "this is final" marker |

The sections below take them in turn.

## Units: dp and pt are not pixels
Android measures layout in density-independent pixels. One dp is one pixel on a 160 dpi screen, and the conversion is dp = px × 160 / dpi, so a 240 px element on a 480 dpi screen is 80 dp (Android's "Grids and units" page). The named density buckets scale in a fixed ratio of 3:4:6:8:12:16, which makes xhdpi 2x, xxhdpi 3x and xxxhdpi 4x. Many real phones sit between buckets. A 420 dpi phone has a density of 2.625, a 440 dpi phone 2.75.

That matters because a value only lands on a whole pixel if it multiplies out evenly. We computed the same 4 dp gap across densities:

| Density | 4 dp | 8 dp | 14 dp |
|---|---|---|---|
| 1.5 (hdpi) | 6 px | 12 px | 21 px |
| 2.0 (xhdpi) | 8 px | 16 px | 28 px |
| 2.625 (420 dpi) | **10.5 px** | 21 px | **36.75 px** |
| 2.75 (440 dpi) | 11 px | 22 px | **38.5 px** |
| 3.0 (xxhdpi) | 12 px | 24 px | 42 px |

At 2.625, only multiples of 8 dp stay whole (8, 16, 24, 32). A 4 dp gap becomes 10.5 px and the system has to round it, so two neighbouring edges can differ by a pixel. This is why "the spacing is off by one pixel" is often not a bug. Tell the developer which scale you design on (a 4 or 8 point grid is the usual choice), say that hairlines should be defined in device pixels where it matters, and agree that a one pixel difference at a fractional density is not a defect.

On iOS the logical unit is the point, and the screen is 2x or 3x. A hairline on a 3x screen is a third of a point, which is a different number from the 0.5 point hairline on a 2x screen. Export icons as vectors so the question disappears, and bitmaps at each platform's multiples.

![A 4 dp gap converted to pixels at five screen densities, with the 2.625 density producing 10.5 pixels.](https://lainagency.com/images/blog-art/app-design-handoff-to-developers-fig2.webp)

## Text: a frame shows one size, users choose another
On iOS, Dynamic Type has seven standard sizes and five larger accessibility sizes (AX1 to AX5). Reference tables that reproduce Apple's text style sizes put body text at 17 pt by default and 53 pt at AX5, a factor of 3.1. We computed the ratios: AX1 is 1.65, AX2 1.94, AX3 2.35, AX4 2.76 and AX5 3.12. On Android, sizes are set in sp, and since Android 14 the system scales text non-linearly: larger text grows more slowly than smaller text, so a size hierarchy survives. The curve is implemented in the `FontScaleConverter` class, and the secondary sources we found say the setting reaches 200%.

Do the check on a real component. A phone frame 390 pt wide with 24 pt side margins leaves 342 pt. A button label that takes 110 pt at the default size takes about 343 pt at AX5. That is just over the 342 pt available, so it wraps. A button drawn 56 pt tall with a one line label will either clip the second line or grow. The handoff has to say which:

- **Buttons and rows:** minimum height, not fixed height. Label wraps to two lines, then truncates with an ellipsis on the third.
- **Tab bars and toolbars:** they cannot grow without eating the screen, so say whether labels hide, icons grow or the bar becomes a list at large sizes.
- **Numbers in a dashboard card:** a big figure that wraps is worse than a smaller one. State whether it shrinks to a minimum or the card stacks.
- **A custom font** must be tied to a text style on iOS (SwiftUI `relativeTo:`), or it will not scale.

Line height needs its own line in the spec. Figma stores line height as a number or a percentage. On Android, Compose adds extra space above the first line and below the last unless `includeFontPadding` is false, and it has been false by default since Compose BOM 2024.01.01. Google's own page notes that trimming with `LineHeightStyle` works only with it off, and suggests `em` units so a percentage in Figma maps directly. If the build was set up before 2024, or pins an old BOM, the text will sit lower than the design.

## The system owns the edges
From Android 15, an app that targets SDK 35 draws under the system bars and display cutouts. Opt-out was possible on Android 15 with `windowOptOutEdgeToEdgeEnforcement`. For apps that target API 36 (Android 16) the attribute is deprecated and disabled, so every Android app will eventually be edge to edge. One blog claims Android 16 enforces it for apps targeting 35 as well, which contradicts Google's page. Trust the behaviour-change page and check which API level your app targets.

The effect on a design: a header with a solid colour that stops at the status bar in a mock-up now has to extend behind it. On iPhones with a Dynamic Island, developer write-ups report a top inset of 59 points and a bottom inset of 34 for the home indicator in portrait, and 0 and 21 in landscape, with 59 at each side. These are device specific, so the rule for the handoff is not "use 59". It is:

1. Draw the background of every screen extending past the safe area.
2. Mark which layers go under the system areas (images, maps, scrolling lists) and which must stay inside (buttons, text, anything tappable).
3. Draw the keyboard open on at least one form.

## Motion: hand over parameters, not a curve
Figma's prototype easing and Smart Animate do not turn into native animation code. Native motion is often a spring, and the two platforms describe it differently. Apple's `Spring` type (iOS 17) can be created from duration and bounce, from response and damping ratio, or from mass, stiffness and damping, and it converts between them. Material's motion tokens are written as a stiffness and a damping ratio.

The values also move. Two community packages that mirror Google's Material 3 expressive tokens disagree on one number, the default spatial damping (0.8 in one, 0.73 in the other). Copy motion values from the platform's own documentation, and write them in the spec as a named token plus the value: "spatial, default: stiffness 380, damping 0.8 (verify against the Material source)". A developer then knows what to change when the platform updates.

For any custom animation, give what moves, the trigger, whether it can be interrupted, and what the reduced motion setting does instead.

## States: use a matrix, not a list
The usual advice is to design loading, empty and error states. That is incomplete for apps, because states multiply. Fleetly's overview is four panels that answer four questions (figures, performance, availability, bookings), so each can be loading, empty or failed while the others are fine.

Components have the same problem in miniature. Draw each state of a component once, next to its name:

![The same black button shown in six states, default, hover, pressed, focus, disabled and loading.](https://lainagency.com/images/blog-art/app-design-handoff-to-developers-fig.webp)

Then build a matrix for each screen and attach it:

| Panel or component | Loading | Empty | Error | Long content | Offline |
|---|---|---|---|---|---|
| Figures | skeleton | zero shown as 0 | retry in panel | 7 digits | last value, marked stale |
| Bookings list | 5 rows | message and action | retry in panel | long names truncate | cached list |

A blank cell means the question has not been asked. For colour that carries meaning, name the tokens by role (`status-running`, `status-moving`, `status-attention`), not by hue. Floware's design uses green, blue and orange for exactly those three conditions, and a role name tells a developer what the colour is for when the brand colour changes. The [design system guide](https://lainagency.com/resources/design-system-for-startups) covers how to set the names up.

## Tokens: what the standard does and does not promise
The Design Tokens Community Group published its format module as version 2025.10 in October 2025. A token's value can be an alias, a reference to another token written in curly braces, and a colour is an object with a colour space and components, not only a hex string. It is a Community Group report, not a W3C standard, and the preview copy on designtokens.org says not to implement that version, so link the final report when you adopt it. The practical value is that one file can feed Figma variables, an iOS build and an Android build, so a colour exists in one place.

## Mark what is final, and know what the marker cannot do
**Ready for dev** is a status you set on frames, sections and components. Figma flags a marked design as Changed when it is edited, and Compare changes lists edited and added layers between versions. Only Full or Dev seat holders on a paid plan can set statuses. Users in Figma's forum report limits worth knowing: the status history can begin at the first time a frame was marked, sections do not record changes as finely as frames, and instance updates may not register as changes. Mark frames rather than sections when you need a record, and write a one line change note when you re-mark something.

Code Connect links components in code to components in Figma, so Dev Mode can show the real one. It needs an Organization or Enterprise plan and a Dev or Full seat. Map the stable components first.

## Review the build with tolerances
"Pixel perfect" is not a test. Agree tiers before the build starts. A tiering published by one engineering team (TSH) is a reasonable template: a layout or structure mismatch is critical, a dimension more than 2 px out or a wrong colour or font is major, and 1 to 2 px of rendering variance is minor. It is one vendor's convention, not a standard, so treat the numbers as a starting point. The fractional density table above is why a 1 px tier exists at all.

Each finding should name the screen, state, device and text size, and record the decision: fix the code or update the design.

## Where it goes wrong
| Symptom | Cause | How to detect | Fix |
|---|---|---|---|
| Spacing is a pixel off on some phones | Values do not multiply to whole pixels at that density | Check the build at 2.625 and 2.75 | Design on an 8 dp grid, accept a 1 px tolerance |
| Layout breaks for some users | Text boxes fixed in height | Run the app at the largest accessibility text size | Minimum heights, wrapping rules |
| Header looks cut off or overlapped by the bar | Background stops at the safe area | Open on a device with a camera cutout | Draw backgrounds under system areas |
| Animation feels different | Duration and easing copied, not a spring | Compare side by side at slow speed | Hand over spring parameters |
| Wrong version built | No marker for final | Ask which frame a feature came from | Ready for dev on frames, with a change note |
| Colours change meaning in dark mode | Tokens named by hue | Switch theme and look at status colours | Name by role |

## Run this readiness test before handing over
Give each line a pass or a fail. Do not hand over with a fail on the first five.

1. Every screen shows its background extending under system areas.
2. Every button, row and card says minimum height and what happens when text wraps.
3. A screenshot exists at the largest text size for at least the three busiest screens.
4. Each list or panel has its own loading, empty and error state in a matrix.
5. Every animation has either a platform token or spring values, plus its reduced motion behaviour.
6. All colours, type styles, spacing and radii use named tokens that match the code.
7. Components show default, pressed, focus, disabled and error where they can occur.
8. Frames to be built are marked Ready for dev and nothing else is.
9. Assets: icons as vectors, bitmaps at each platform's multiples.
10. A tolerance and an owner for differences are agreed.

On our [app and dashboard projects](https://lainagency.com/about#app-design), which typically take 8 to 12 weeks, the final delivery phase includes file preparation, guidelines and organised assets. [Floware](https://lainagency.com/works/floware) and [Fleetly](https://lainagency.com/works/fleetly) are two dashboards we have designed, and [Aerium](https://lainagency.com/works/aerium) includes a mobile app. If you have an app to design or a design to hand over, [talk to us](https://lainagency.com/contact). For the platform sizes themselves, see the [mobile app design guide](https://lainagency.com/resources/mobile-app-design-guide), and for the accessibility side, the [accessibility checklist](https://lainagency.com/resources/website-accessibility-checklist).

## Frequently asked questions

### What should an app design handoff include?

A file organised by flow with real components, named tokens, a matrix of states per screen, rules for text that grows, which layers go under system bars, motion as spring values or platform tokens, exported assets, and a marker for which frames are final. A prototype helps but the written rules are the specification.

### Why does my developer's spacing differ by one pixel from my design?

Android measures layout in dp, and some phones have fractional densities such as 2.625 (420 dpi). A 4 dp gap is then 10.5 pixels and the system rounds it. Design on an 8 point grid, which stays whole at 2.625, and agree a one pixel tolerance.

### How do I design for large text on iOS and Android?

Use minimum heights instead of fixed ones and decide how each component wraps or truncates. iOS Dynamic Type reaches about 3.1 times the default body size at the largest accessibility setting. Android scales text non-linearly from Android 14, so large sizes grow more slowly. Test the busiest three screens at the largest setting.

### What does Ready for dev mean in Figma and what are its limits?

It is a status for frames, sections and components that tells developers a design is final. Figma marks edited ones as Changed and offers Compare changes. Users report the status history can start at the first marking and that sections record less detail than frames, so use frames and add a change note.

### Do I need to design edge to edge on Android?

Yes. Apps targeting SDK 35 draw under system bars by default, and for apps targeting API 36 the opt-out attribute is disabled. Draw backgrounds under the status and navigation bars and keep tappable content inside the insets.

### Are design tokens a W3C standard?

No. The Design Tokens Format Module 2025.10 is a final Community Group report, not a W3C Recommendation. It defines aliases and colour objects with a colour space, and it is meant for moving tokens between tools.

## Sources and fact check

Checked on 28 Oct 2026.

- [Android Developers: Support different pixel densities](https://developer.android.com/training/multiscreen/screendensities)
- [Android Developers: Grids and units](https://developer.android.com/design/ui/mobile/guides/layout-and-content/grids-and-units)
- [Android Developers: Behavior changes, Android 16](https://developer.android.com/about/versions/16/behavior-changes-16)
- [Android Developers: Display content edge to edge and handle insets in Compose](https://developer.android.com/jetpack/compose/layouts/insets)
- [Android Developers: FontScaleConverter reference](https://developer.android.com/reference/android/content/res/FontScaleConverter)
- [Android Developers: Style paragraph text in Compose](https://developer.android.com/jetpack/compose/text/style-paragraph)
- [Apple Developer: Spring (SwiftUI)](https://developer.apple.com/documentation/swiftui/spring.md)
- [DEPT Engineering: iOS accessibility part 1, Dynamic Type](https://engineering.deptagency.com/ios-accessibility-part-1-dynamic-type)
- [Design Tokens Community Group: Design Tokens Format Module 2025.10](https://www.w3.org/community/reports/design-tokens/CG-FINAL-format-20251028/)
- [Figma Help: Dev Mode statuses and notifications](https://help.figma.com/hc/en-us/articles/26781702258583-Dev-Mode-statuses-and-notifications)
- [Figma Developers: Code Connect](https://developers.figma.com/docs/code-connect)
- [Figma Forum: Compare changes in Dev Mode, inconsistent behavior](https://forum.figma.com/t/compare-changes-in-dev-mode-inconsistent-behavior/91005)
- [Sizzy: Design to dev handoff best practices](https://sizzy.co/blog/design-to-dev-handoff)
- [TSH: UI verification severity tiers](https://copilot-collections.tsh.io/docs/skills/ui-verification)

