Character · Today · Choices · Text · Colour · Symbols · Shape & space · Components · Placement · Motion & words · Accessibility · Enforcement

Design System

The rules every screen follows. Views use tokens, never raw values: no .font(.headline), no .red, no cornerRadius: 10, no .padding(7).

Tokens live in Code/DesignSystem.swift, shared parts in Code/Components.swift, and Code/swiftlint-design.yml fails the build on anything else. The pictures on this page are crops of the redesign mockup, which is built from those two files, shown at their real point size. Everything here is a proposal until Gerry picks.

Character

A field guide for intermediate birders: people who know a robin from a crow, own binoculars, and can't reliably split sparrows. They use it outdoors, one-handed, in bright light, often on one bar of signal. The photos of birds are the star, not the map and not the chrome. It should feel like a good printed field guide: white pages, clear type, the family on every page, and nothing that shouts except a rare bird.

Photos first

White sheets, big photos, thin frames. Nothing is coloured for decoration, so the birds bring the colour.

Readable in sun

Light is the reference design. Every text colour clears 4.5:1, and secondary text only sits on the plain sheet.

Big targets

Nothing tappable under 44 points, box buttons 60 tall, and the pickers you use most are real buttons, not small text.

Today's app, counted

An audit of every raw value in the app's 113 Swift files on September 30, 2026 (commit 6ac3e0c). Nothing is wildly wrong on its own; together it adds up to an app with no rules.

WhatCountThe worst of it
Text styles14 styles, 34 placesSection headings and list rows are both plain .headline. The scientific name is styled two ways.
Colours16 colours, 73 placesThe accent is system blue, but the icon and the brand are orange. The accent is written three ways. Red means four things: the location warning, every sighting on the map, a destructive button, and Critically Endangered.
Brand orange1 use#FF6800, with white text on it at 2.9:1, which fails. It isn't the icon's orange (#F98F18) either.
Corner radii5 values, 10 placesOne radius, 10, rounds the buttons, the sheet itself and the app icon. Thumbnails are 3, photos 5, a tag 4 (borrowed from a padding).
Padding13 amounts, 38 placesFour are negative offsets to undo other padding, one marked "TODO: fix this hack".
Animations8 kinds, 25 placesThe sheet snaps with .snappy, scrolls with .spring, the title with .easeInOut, and 14 places take the default.
Symbols21 in usesquare.and.arrow.up means both Share and Open Website. Errors use a triangle for photos and a circle for audio. A weather symbol sizes the menu icons.
Shadows2Radius 1, default colour, in two offsets.

Choices for Gerry

Each choice is drawn three ways on the same content. The proposed option is marked; the rest of this page is built on it. Tap to see them big.

Corner radius options
Corners: 4·8·12, 6·10·16, 8·14·22
Button style options
Buttons: text links, box buttons, glass capsules
Species row options
Species row: today, field guide row, photo grid
Map annotation options
Map: today's red balloons, count discs, dots
Light and dark: both, light first. The app follows the system setting. Light is the reference because the app is used in sunshine; dark is for dawn and owling. Proposed, not decided.
Accent: the icon's sunset orange. #F98F18 for fills, with the geese's near-black #321B03 as ink on it. Proposed, not decided. Blue goes.

Text

Twelve roles, applied with .textRole(_:). Each is a Dynamic Type style, so all text scales with the user's text size. Samples are at the default size.

RoleSampleStyleFor
titleAnna's HummingbirdTitle 2, boldEvery sheet's title, always with a subtitle
subtitleVancouver, BC · 113 speciesSubheadline, secondaryThe line under a title
scientificCalypte annaSubheadline, italic, secondaryEvery scientific name, the one way
sectionRecent sightingsTitle 3, boldHeadings inside a sheet
nameGolden-crowned SparrowHeadlineA species in a row or card
bodyStanley Park--Lost LagoonBodyArticle text, other rows
detail2 days ago · 3 birdsSubheadline, mono digits, secondaryWhen and how many
captionFrom Wikipedia · CC BY-SACaption, secondaryAttributions, footers
labelNew World SparrowsCaption, bold, uppercase, 0.6 trackingFamily headers and tags only
buttonListenSubheadline, semibold, Title CaseBox buttons, pickers
badgeLocation OffFootnote, semiboldMap badges
mapLabelJericho Beach ParkCaption, bold, with a haloNames on the map
A bird has two names. The common name is name or title; the scientific name is always scientific, italic and never bold, the way every field guide prints it.
Uppercase is for metadata. Family headers and tags are label. Buttons are Title Case button. Never call .uppercased().
Numbers don't jitter. Counts, distances and times use mono digits.
One escape hatch. The count inside a map disc is drawn at a fixed size with Font.drawn(_:), because it scales with the disc, not with Dynamic Type.

Colour

Every colour has one job, and goes through Palette. Ratios are WCAG contrast, measured.

#F98F18
accentFills only: the primary button, the tick, sightings on the map
#321B03
onAccentInk on accent: 6.9:1. White on it would be 2.3:1
#A34A00 · #FFA64D
accentTextLinks, pickers, box button text: 5.9:1 on white, 8.8:1 on dark
#B0125B · #FF5C9D
rare · onRareRare birds, in lists and on the map. White ink 6.8:1; black in dark, 7.3:1
systemRed
dangerErrors and destructive actions only. Never on the map
#FFFFFF · #1C1C1E
sheetWhite, so photos sit on paper, not grey
#F4F0EA · #2C2C2E
raisedCells, rows, chips, box buttons: warm, like field guide paper
radiusRingThe search area: a dashed ink ring, no fill, 14:1 on the map

Conservation status

The IUCN Red List's own colours, so they match every other field guide that uses them.

LC NT VU EN CR EW EX
Orange is a fill, not text. The icon's orange is 2.3:1 on white, so it only ever fills a shape with dark ink on it. Orange text is the darker accentText.
One accent per sheet. A sheet has at most one orange button. The rest are raised.
Magenta means rare. The tag in a list and the disc on the map are the same colour, and nothing else uses it.
Red means something is wrong. Location off, a failed load, Delete. A sighting is never red.

Symbols

SF Symbols only, named in Symbol. Two ideas never share a symbol, and one idea never has two. On their own they come in two sizes: IconSize.control (17 pt semibold) for map buttons and header circles, and IconSize.large (22 pt semibold) for box buttons and the tick.

IdeaSF SymbolIdeaSF Symbol
Nearbybinoculars.fillSearchmagnifyingglass
Rare Bird Alertexclamationmark.bubble.fillRare tagstar.fill
My ListchecklistMenuline.3.horizontal
Seen it (ticked)checkmark.circle.fillNot tickedcircle
ListenwaveformPlay · Pauseplay.fill · pause.fill
Sharesquare.and.arrow.upOpen a websitearrow.up.right.square
My locationlocation.fillLocation offlocation.slash.fill
Radiuscircle.dashedDays backcalendar
Sortarrow.up.arrow.downA pickerchevron.up.chevron.down
Sightings, a placemappin.and.ellipsePhotosphoto.on.rectangle
Settingsgearshape.fillAboutinfo.circle.fill
Supportheart.fillOfflinewifi.slash
Any errorexclamationmark.triangle.fillClosexmark

Shape & space

Corners: 6 · 10 · 16. Chosen from three sets on the proposal sheet: 4·8·12 felt like a tool, 8·14·22 turned photos into stickers.
Boxes are for work, rounds are for floating. Things inside a sheet are rounded rectangles: photos, cells, box buttons. Things floating over the map are circles and capsules: map buttons, badges, sighting discs.
small · 6

Tags, the conservation code

medium · 10

Photos, thumbnails, cells, box buttons

large · 16

The sheet and the iPad column

A 4-point grid

xxs · 2
xs · 4
s · 8: between buttons, cells and photos
m · 12: between a thumbnail and its text, inside a cell
l · 16: sheet content from the sheet's edge
xl · 24: between sections

Sizes

TokenPointsFor
touch44Nothing tappable is smaller
thumbnail56A species photo in a row (today 50)
photoStrip · photoStripLarge132 · 220The photo strip at half and full height (today 120 · 200)
boxButton60A box button's height
marker28A sighting disc on the map

Components

Build sheets from these. A new part gets added here before it is used twice.

Sheet header

SheetHeader

Every sheet's top row: a title, a subtitle that says something useful (the locality and count, or the scientific name), and a close circle. It is what shows when the sheet is small.

Picker chips

PickerChip

Radius, days back and sort, always in the Nearby header. They look like today's text pickers but are 44 points tall, on a raised capsule, so a cold thumb hits them.

Family header and species rows

FamilyHeader and SpeciesRow

Species sit under their family in field-guide order. A row is a 56-point photo, the name, then when and how many in detail, a Rare tag when eBird flags it, and the tick. The tick is the only way onto My List.

Photo strip

Photo strip

The first thing on a species. The lead photo is Wikipedia's, which is curated, then Flickr's. The first photo is wider. Tap for full screen.

Action row

ActionRow of BoxButtons

Up to four boxes: IconSize.large over button text in Title Case. The first may be primary, in orange with dark ink. The rest are raised.

Listen
Seen It
Share
Recent sightings

Recent sightings

A section heading that states its scope ("5 km · 14 days"), then one row per place with the same disc the map draws. Tap a row and its disc grows on the map; tap a disc and its row is highlighted.

Row group

RowGroup of ListRows

Menus, settings and links: rows on one raised block with the medium corner. Symbol in accentText (magenta for Rare Bird Alert), body title, optional detail, and a chevron when it opens something.

Empty state

EmptyState

What a sheet shows with nothing in it: a symbol, one line that says why, and one primary way out. Every sheet has one, and an offline version that shows the last list and its time.

Recording row

Recording row

A round play button, the type as name, place and length as detail, the recordist and licence as caption. The one playing is orange and shows its waveform.

Map buttons

MapButton, MapBadge and SightingMarker

Floating on the map. Map buttons are 44-point glass circles, stacked just above the sheet's trailing edge so they're in thumb reach. Badges are capsules: red for something wrong, orange for My List and Support, glass for quiet status. Sighting discs: 3 a sighting, ★ a rare one.

Placement

The map is the app

Every surface is a sheet over the map. The top sheet decides what the map shows. Only the full-screen photo viewer and the share sheet cover it.

Two taps to a bird

Launch opens Nearby. Tap a bird and you're there. The stack is never more than three deep.

Controls in the bottom half

Pickers in the sheet header, map buttons above the sheet. Nothing you use often lives at the top of the screen.

The knobs stay on Nearby

Radius, days and sort are in the Nearby header, never in Settings. Settings has three things.

One primary action

The first box button, in orange. One per sheet.

Destructive is red and last

Discard a list, remove a bird: red text, at the bottom.

Motion & words

Motion

One animation, Animation.standard: snappy, 0.25 seconds, no bounce, for every state change, including the sheet's snap. Nothing moves on its own except the map and a playing recording. Respect Reduce Motion.

Words

Title Case for titles and buttons, sentence case for everything else. A button is one or two words: "Listen", "Seen It", "Share". Say what happened, plainly: "Nobody has reported birds within 1 km in the last 3 days." Same thing, same word: sighting, species, My List, Rare.

Accessibility

Enforcement

Code/swiftlint-design.yml has seven custom rules, all errors, all skipping DesignSystem.swift.

✕ Raw valuesVStack(spacing: 10) { Text("LISTEN") .font(.caption2.weight(.bold)) .foregroundStyle(.orange) .uppercased() Image(systemName: "speaker.wave.3.fill") } .padding(14) .background(Color(white: 0.24), in: .rect(cornerRadius: 12)) withAnimation(.spring) { … }
✓ TokensVStack(spacing: Space.xs) { Image(systemName: Symbol.listen) .font(IconSize.large) Text("Listen") .textRole(.button) } .padding(Space.m) .background(Palette.raised, in: .rect(cornerRadius: Radius.medium)) withAnimation(.standard) { … }
RuleCatchesUse instead
design_font.font(.headline), .font(.system(size:)).textRole, IconSize, Font.drawn
design_colourColor(white:), .accentColor, .foregroundStyle(.red), .brandOrangePalette
design_radiuscornerRadius: 10, .cornerRadius(5)Radius
design_spacingspacing: 10, .padding(14), .padding(-10)Space
design_uppercase.uppercased(), .tracking(), .textCase().textRole(.label)
design_animation.spring, .easeInOut, .snappyAnimation.standard
design_symbolImage(systemName: "gear")Symbol

Proven

Run over DesignSystem.swift and Components.swift, the rules find nothing. Run over the file on the left, every rule fires and SwiftLint exits 2:

Violation.swift:8:16: error: Design system: spacing Violation: Use a Space value. (design_spacing)
Violation.swift:10:17: error: Design system: text role Violation: Use .textRole(_:) for text, IconSize for symbols, or Font.drawn(_:) inside a drawing. (design_font)
Violation.swift:11:17: error: Design system: palette Violation: Use a Palette colour. (design_colour)
Violation.swift:12:17: error: Design system: uppercase Violation: Uppercase text is the .label role; write the string in Title Case. (design_uppercase)
Violation.swift:13:19: error: Design system: symbol Violation: Name the symbol in Symbol, one symbol per idea. (design_symbol)
Violation.swift:15:9: error: Design system: spacing Violation: Use a Space value. (design_spacing)
Violation.swift:16:21: error: Design system: palette Violation: Use a Palette colour. (design_colour)
Violation.swift:16:51: error: Design system: radius Violation: Use .clipShape(.rect(cornerRadius: Radius.small, .medium or .large)). (design_radius)
Violation.swift:17:25: error: Design system: motion Violation: Use Animation.standard. (design_animation)

Run over today's app, they find 81 errors: 31 fonts, 17 colours, 16 paddings and spacings, 11 radii, 3 animations, 2 symbols and 1 uppercase. That is the migration list. (The rules catch the obvious forms; the audit above counts every use.)


Birds Near Me 2 • Send Feedback • Birds Near Me 1 • © 2026 Gerry Shaw