Designing in strict monochrome

Thiết kế monochrome nghiêm ngặt

uranashel apps get read in bad places. 11 PM in a parking garage, one hand still on the steering wheel. At 6 AM the same phone is a sleep timer being poked by someone half-asleep. On a factory floor the operator wears gloves and has no patience for decorative chrome. Each of those scenes contains exactly one number the user came for, and every other pixel is either helping them find it or standing in the road.

Color usually stands in the road. Every hue competes for attention with the reading you opened the app to see, so we treat color as cognitive load and spend it the way we spend battery. The rule is strict: black, white, and gray only, plus 3 semantic chromatic exceptions. Green marks success and acceptable states. Yellow marks caution and degraded confidence. Red marks danger and over-limit conditions. That is the entire palette across all 5 apps. No brand purple, no SwiftUI default blue tint. And no Material You dynamic accent that restyles itself from your wallpaper; the wallpaper does not get a vote in an instrument.

The reason is instrument design

The constraint is not aesthetic minimalism for Instagram screenshots. It is instrument design. A decent multimeter has one visual accent, the needle, and everything else on it is matte. Sonarish displays LAeq trending toward a legal noise threshold, and that decibel number should command the whole screen; a gradient hero banner around it would be decoration stealing glances from data. LAeq is an A-weighted equivalent level, so the digits already encode a model of human hearing.

Dark is the default theme. Partly that is the garage at 11 PM. Partly it is Estua: full dark adaptation of the eye takes 20–30 min to build, and one white screen at 2 AM erases most of it, which means a sleep app that blinds you while you set the timer has failed at its main job. Light theme exists as a pure inversion for daylight, sharing identical structure with dark and differing only in luminance.

Color that carries data

The 3 exceptions work hard. Wheria's confidence badge uses green, yellow, and red, tied directly to the estimation standard deviation coming out of the fusion filter. Yellow there is a statistical claim about position uncertainty. A decorative mood indicator would not survive review in this studio. When covariance grows past the first gate the badge steps from green to yellow; past the second gate it goes red and the app tells you plainly to trust the arrow less.

Sonarish applies the same mapping to noise dose. A shift log trending toward the NIOSH 8-hour limit renders the accumulating percentage in plain gray while the projection stays under the limit, then red once it crosses. Green never appears on that screen. A dose at 79% of the limit is merely tolerable, and gray covers tolerable.

Phyzix keeps the discipline inside its graphs. Accelerometer, gyroscope, and magnetometer traces are distinguished by line weight and dashing. An early build tried rainbow colormaps and they died the first time a teacher projected a lesson, because on a washed-out classroom projector nobody could tell which axis was which. Line style survives projection, grayscale printing, and every form of color blindness at once.

We also rejected accent-color pickers entirely. A theming menu lets every user make their instrument look different from every screenshot in the manual, and then support tickets arrive with images we cannot match to our own builds. One palette. Two themes. That discussion was short.

Grays, tokens, one lint rule

Between black and white sit 5 named grays, each defined by role: primary text, secondary text, hairline borders, card surfaces, disabled states. No line of code refers to a raw hex value; every color routes through one Theme file per platform, and a CI grep fails the build when a color literal appears anywhere else. The rule sounds petty. It is also the only reason the palette is still monochrome after 5 apps and 2 platforms' worth of pull requests.

Pure black backgrounds turned out to be a mistake on OLED. On 2 of our test phones, light text scrolling over #000 left a brief smear, because OLED pixels switching on from fully off respond slower than pixels already lit. Lifting the dark background to a near-black gray removed the ghosting. Bench note: 2 OLED devices, 1 evening, a scrolling test page, slow-motion footage at 240 fps.

Depth needed a rethink as well. Material Design expresses elevation with drop shadows and SwiftUI leans on translucent layering, and both go muddy under strict monochrome. We use 1 px hairline borders plus a 1-step surface lift: a card in dark theme sits on a gray one step lighter than the background, and in light theme one step darker.

SwiftUI fights back

Enforcing monochrome on iOS meant fighting the framework's defaults at every layer. The root view gets .tint(Theme.primaryText), which kills system blue on buttons, links, and toggles. Delete that single modifier and every interactive element reverts to Apple's accent by the next build. The text-insertion cursor held out longest; tint only reaches it on recent iOS versions, so for 2 minor releases we shipped a monochrome app with 1 blinking blue survivor.

Fonts fought harder. Navigation titles ignore UINavigationBar appearance font settings on iOS 16 and later, so we inject Nunito through a principal toolbar item using our custom nunitoTitle modifier. Toolbar buttons need explicit font assignment or they silently revert to SF Pro, and silent is the expensive part: nothing logs, nothing crashes, the title is simply wrong 3 screens deep. Form and List screens default to 17 pt system font while the application body runs 15 pt Nunito, so every Settings, AddEditVehicle, and BrandPicker screen sets its subheadline font explicitly. Miss one screen and its rows look like they belong to a different application.

Compose mostly cooperates

Android was gentler. Compose routes nearly everything through MaterialTheme, and our AppTypography maps all text roles to Nunito in one declaration. Third-party composables occasionally leak Roboto, so screens hosting them get a defensive ProvideTextStyle wrapper. Some surfaces we cannot own at all: TimePickerDialog, Toast messages, and system permission sheets stay in the system font because the operating system draws those popups itself. We stopped arguing with it.

Corner radius is 12 px everywhere: cards, buttons, bottom sheets. Not 8, not 16. Across 5 applications the sameness is the point, because users subconsciously trust instruments that look like they came from the same laboratory. A radius that drifts between apps is a small tell that nobody is checking details, and a user who notices one loose detail starts doubting the numbers next.

Light in a garage, measured

Contrast targets came from the field before they came from any spec sheet. We walked our usual test garage with a lux meter, 1 phone running a Phyzix build cross-checked against a cheap standalone meter, 1 evening plus 1 noon visit. Roughly 38 lux on the B2 ramp, 12 lux near pillar E9, and 31,000 lux in the open-air lot at 1 PM. The same screen has to survive a bit over 3 orders of magnitude of ambient light.

So primary text targets WCAG AA contrast in both themes, and secondary text has to stay readable without squinting on a dim garage ramp and in a bright outdoor parking lot alike. In the dark theme, primary text sits near 15:1 against the near-black surface and secondary text near 7:1. The light theme lands on the same ratios by construction, because it is the same token set with luminance inverted.

State never rides on color alone

Roughly 8% of male users have some form of color vision deficiency. A yellow warning invisible to a deuteranope is a lying instrument, so we never rely on color alone to convey state. Sonarish pairs a red threshold breach with haptic feedback and an icon change, which reaches you even with the phone face-down in a shirt pocket. Wheria's compass confidence indicator carries a text label such as "Low compass trust" alongside the yellow chip. The label costs a little width. The lie would cost more.

What we refuse to ship

Gradient hero sections. Glassmorphism overlays, neon augmented-reality decorations, anything that treats the camera feed as a canvas. Wheria's AR direction arrow renders white with a thin black stroke over the feed, a pairing that stays readable on concrete, asphalt, and the painted lines of open-air lots, which is close to every background a parked car generates. The feed itself stays unmodified. Heat-map tints and color-coded distance rings would turn a navigation aid into a video game.

The cost of all this restraint is real and we accept it. On a store page our screenshots sit quietly next to neon competitors, and quiet tends to lose the first-glance comparison. What it wins is the 400th session, when the user stops seeing the interface and just reads the number.

Estua's sleep timer and scene picker follow the same visual vocabulary as Wheria's Find screen and Sonarish's meter readout, so a user switching between uranashel apps at 2 AM never has to relearn where the important number lives on screen. That consistency only exists because no single app is allowed personality at the expense of the set. Typography carries the hierarchy that color would otherwise carry; the details live in Nunito on every pixel, and the release discipline that keeps the iOS and Android versions visually aligned is covered in shipping iOS and Android at the same time.

Underneath sits the same philosophy that drives our sensor work: an interface must stay honest even when honesty looks plain, the thread that also runs through indoor navigation when GPS dies. Monochrome belongs to instrument integrity as one more component, next to the filter tuning and the calibration routine, and it gets maintained with the same seriousness.

App của uranashel bị đọc ở những chỗ khó chịu. 11 giờ đêm trong hầm xe, một tay còn trên vô lăng. 6 giờ sáng, nửa tỉnh nửa mê, mò nút timer ngủ. Trong xưởng máy, operator đeo găng tay và không rảnh ngắm chrome trang trí. Cảnh nào cũng chỉ có đúng một con số user cần xem. Mọi pixel khác hoặc giúp tìm ra nó hoặc đứng chắn đường.

Màu thường là thứ chắn đường. Sắc màu nào cũng giành attention với con số bạn mở app để đọc, nên team coi màu là tải nhận thức và tiêu nó dè sẻn như tiêu pin. Luật rất chặt: chỉ đen, trắng và xám, cộng 3 ngoại lệ semantic. Xanh lá cho trạng thái ổn và thành công. Vàng cho mức cảnh giác. Đỏ cho nguy hiểm và vượt ngưỡng. Hết, cả 5 app chung đúng palette đó. Không tím brand, không tint xanh mặc định của SwiftUI, không accent Material You đổi theo wallpaper. Wallpaper không có quyền bỏ phiếu trong một dụng cụ đo.

Lý do: thiết kế dụng cụ đo

Ràng buộc này không phải minimalism để chụp màn hình sống ảo. Đây là thiết kế dụng cụ đo. Đồng hồ vạn năng tử tế chỉ có một điểm nhấn thị giác là cây kim, phần còn lại đều lì. Sonarish hiển thị LAeq đang tiến về ngưỡng ồn pháp lý; con số decibel phải chiếm trọn màn hình, banner gradient bao quanh chỉ ăn cắp ánh nhìn khỏi dữ liệu. LAeq là mức tương đương theo trọng số A, mấy chữ số đó tự thân đã gói một mô hình thính giác của người.

Dark là theme mặc định. Một phần vì hầm xe lúc 11 giờ đêm. Một phần vì Estua: mắt cần 20–30 phút mới thích nghi bóng tối hoàn toàn, một màn hình trắng lúc 2 giờ sáng xóa gần hết công sức đó. App ngủ mà làm chói mắt người đang chỉnh timer là hỏng ngay việc chính. Theme light là bản đảo thuần cho ban ngày: cùng cấu trúc với dark, chỉ khác luminance.

Màu phải chở dữ liệu

3 ngoại lệ phải làm việc nặng. Badge confidence của Wheria dùng xanh, vàng và đỏ, gắn thẳng vào độ lệch chuẩn ước lượng lấy từ bộ lọc fusion. Vàng ở đây là một phát biểu thống kê về độ bất định vị trí. Chỉ báo tâm trạng trang trí không sống nổi qua review của studio này. Covariance vượt cổng thứ nhất, badge bước từ xanh sang vàng; vượt cổng thứ hai thì đỏ. App nói thẳng là nên bớt tin mũi tên.

Sonarish áp cùng cách map cho liều ồn. Log ca làm tiến về định mức NIOSH 8 giờ thì phần trăm tích lũy hiện xám bình thường khi dự phóng còn dưới định mức, vượt qua mới chuyển đỏ. Xanh lá không bao giờ xuất hiện ở màn đó. Liều ồn 79% định mức chỉ là mức chịu được. Xám lo đúng phần đó.

Đồ thị Phyzix giữ cùng kỷ luật. Trace của gia tốc kế, con quay và từ kế phân biệt bằng độ dày nét và nét đứt. Bản build đầu từng thử colormap cầu vồng, chết ngay lần đầu giáo viên chiếu bài: máy chiếu lớp học bạc màu, chẳng ai biết trục nào là trục nào. Kiểu nét thì sống qua máy chiếu, qua bản in trắng đen và qua mọi dạng mù màu cùng lúc.

Picker accent color bị loại hẳn. Cho user tự đổi màu nghĩa là mỗi máy trông khác mọi screenshot trong tài liệu hướng dẫn, rồi ticket support gửi về những ảnh team không khớp nổi với build của chính mình. Một palette. Hai theme. Cuộc tranh luận hôm đó ngắn.

Xám, token và một luật lint

Giữa đen và trắng có 5 mức xám đặt tên theo vai trò: text chính, text phụ, viền hairline, bề mặt card, trạng thái disabled. Không dòng code nào ghi hex trực tiếp; mọi màu đi qua một file Theme cho mỗi nền tảng; CI grep thấy literal màu nằm ngoài file đó là đánh fail build. Luật nghe nhỏ nhặt. Nhưng nhờ đúng nó mà palette vẫn monochrome sau 5 app và 2 nền tảng đầy pull request.

Nền đen tuyền hóa ra là sai lầm trên OLED. Trên 2 máy test, chữ sáng cuộn trên #000 để lại vệt mờ thoáng qua: pixel OLED bật lên từ trạng thái tắt hẳn phản hồi chậm hơn pixel đang sáng sẵn. Nâng nền dark lên xám gần đen là hết ghosting. Ghi chú bench: 2 máy OLED, 1 buổi tối, một trang test cuộn và footage slow-motion 240 fps.

Chiều sâu cũng phải nghĩ lại. Material diễn tả elevation bằng bóng đổ, SwiftUI dựa vào lớp mờ xuyên sáng, cả hai đều đục khi vào monochrome nghiêm ngặt. Team dùng viền hairline 1 px cộng nâng bề mặt 1 bậc: card trong theme dark nằm trên nền xám sáng hơn 1 bậc, sang theme light thì tối hơn 1 bậc.

SwiftUI chống trả

Ép monochrome trên iOS là đánh nhau với default của framework ở từng lớp. View gốc gắn .tint(Theme.primaryText) để giết xanh hệ thống trên button, link và toggle. Xóa đúng một modifier đó thì build sau, mọi phần tử tương tác quay về accent của Apple. Con trỏ nhập text cầm cự lâu nhất: tint chỉ chạm tới nó trên iOS đời mới, nên có 2 bản minor team ship app monochrome với đúng 1 kẻ sống sót màu xanh nhấp nháy.

Font còn lì hơn. Title navigation bỏ qua thiết lập font qua UINavigationBar appearance từ iOS 16, nên Nunito phải inject qua toolbar item principal bằng modifier nunitoTitle tự viết. Button trên toolbar phải gán font tường minh, không thì lặng lẽ quay về SF Pro; chữ lặng lẽ mới tốn kém, vì không log, không crash, chỉ thấy title sai ở màn thứ 3 nào đó. Form và List mặc định 17 pt system trong khi body app chạy Nunito 15 pt, nên mọi màn Settings, AddEditVehicle và BrandPicker đều set font subheadline bằng tay. Sót một màn là hàng trong đó trông như của app khác.

Compose dễ tính hơn

Bên Android nhẹ hơn nhiều. Compose dồn gần hết qua MaterialTheme, AppTypography map toàn bộ vai trò text sang Nunito trong một chỗ khai báo. Composable bên thứ ba thỉnh thoảng rò Roboto, màn nào chứa chúng thì bọc thêm ProvideTextStyle phòng thủ. Có những bề mặt team không sở hữu nổi: TimePickerDialog, Toast và sheet xin quyền hệ thống vẫn chạy font hệ thống, vì OS tự vẽ mấy popup đó. Thôi, không cãi nữa.

Bo góc 12 px khắp nơi: card, button, bottom sheet. Không 8, không 16. Trên 5 app, giống nhau chính là mục đích, vì user tin ngầm những dụng cụ trông như ra từ cùng một phòng lab. Bán kính lệch giữa các app là dấu hiệu nhỏ rằng không ai soát chi tiết, mà user đã bắt gặp một chi tiết lỏng thì sẽ nghi tiếp đến con số.

Đo ánh sáng trong hầm

Chỉ tiêu contrast đến từ thực địa trước khi đến từ spec. Team đi một vòng hầm test quen với lux kế: 1 máy chạy build Phyzix, đối chiếu thêm một máy đo rời giá rẻ, 1 buổi tối cộng 1 buổi trưa. Kết quả khoảng 38 lux ở ramp tầng B2, 12 lux gần cột E9, còn bãi ngoài trời lúc 1 giờ chiều lên 31.000 lux. Cùng một màn hình phải sống qua hơn 3 bậc độ lớn ánh sáng môi trường.

Vì vậy text chính nhắm contrast WCAG AA ở cả hai theme, text phụ phải đọc được không cần nheo mắt, trên ramp hầm tối cũng như ngoài bãi nắng gắt. Theme dark: text chính khoảng 15:1 so với nền gần đen, text phụ khoảng 7:1. Theme light ra đúng các tỷ lệ đó theo cách dựng, vì dùng cùng bộ token chỉ đảo luminance.

Trạng thái không đi một mình trên màu

Khoảng 8% nam giới mang một dạng khiếm khuyết thị giác màu. Cảnh báo vàng mà người deuteranope không nhìn thấy là một dụng cụ nói dối, nên team không bao giờ truyền trạng thái chỉ bằng màu. Sonarish ghép vượt ngưỡng đỏ với haptic và đổi icon, điện thoại úp mặt trong túi áo vẫn báo tới nơi. Chỉ báo confidence la bàn của Wheria kèm nhãn chữ kiểu "Low compass trust" cạnh chip vàng. Nhãn tốn chút bề ngang. Lời nói dối tốn nhiều hơn.

Những thứ nhất quyết không ship

Hero gradient. Overlay glassmorphism, trang trí AR neon, mọi thứ coi camera feed là chỗ để vẽ. Mũi tên chỉ hướng AR của Wheria vẽ trắng viền đen mỏng đè lên feed, cặp màu đó đọc được trên bê tông, nhựa đường và vạch sơn của bãi hở, gần đủ mọi phông nền một chiếc xe đỗ có thể tạo ra. Bản thân feed giữ nguyên không chỉnh. Tint heat-map với vòng khoảng cách nhiều màu sẽ biến công cụ dẫn đường thành video game.

Cái giá của kiềm chế là thật và team chấp nhận. Trên store, screenshot của mình đứng im lặng cạnh đối thủ neon, mà im lặng thường thua vòng liếc đầu tiên. Thứ nó thắng là phiên dùng thứ 400, khi user không còn thấy giao diện nữa, chỉ đọc con số.

Timer ngủ và picker scene của Estua nói cùng ngôn ngữ thị giác với màn Find của Wheria và màn readout của Sonarish, nên đổi app uranashel lúc 2 giờ sáng bạn không phải học lại xem con số quan trọng nằm đâu. Nhất quán kiểu đó chỉ tồn tại vì không app nào được phép có personality riêng bằng cái giá của cả bộ. Typography gánh phần hierarchy mà màu lẽ ra gánh; chi tiết nằm ở Nunito trên mọi pixel, còn kỷ luật release giữ bản iOS và Android khớp nhau về visual thì ở ship iOS và Android cùng lúc.

Bên dưới vẫn là triết lý chạy xuyên mảng cảm biến của team: giao diện phải trung thực kể cả khi trung thực trông đơn sơ, mạch này cũng chạy qua dẫn đường trong nhà khi GPS chết. Monochrome nằm trong tính toàn vẹn của dụng cụ đo như một linh kiện nữa, cạnh việc tune bộ lọc và quy trình hiệu chuẩn, được bảo trì nghiêm túc y như vậy.