Sift

Swipe your screenshots into three piles.

The app shows one screenshot at a time, newest unreviewed first. Left trashes, right archives, up favourites — and one rewind undoes the last call. Everything on this page comes from the Swift token files; the block you are reading is the onboarding block, tomato with navy ink.

Bold Playful Three verbs
01

Principles

Global rules, in force on every screen: no borders (the focus ring is the one exception) · no emoji · no icon tiles · no color-only meaning · no dark mode.

The screenshot is the hero

The card is the product. Chrome stays out of its way: a counter, two icon buttons, a verdict row. No tab bar, no toolbars, no second layout system.

Three verbs, three colors, never color alone

Trash, Archive, Fave. Direction, glyph and word carry the meaning; hue is the third channel, so the system still works for a deuteranope and in a screenshot.

One queue, one step back

A single queue, newest unreviewed first, and exactly one level of undo. Deeper mistakes are handled by Trash restore and by moving items in Library.

Destruction is confirmed by the system

Verdicts are reversible and need no confirmation. Permanent deletion always ends in the iOS dialog; deleting everything adds an in-app sheet that names the count first. The size rides on the docked button that opens it, not in the sheet.

Saturation belongs to verdicts and blocks

Chrome is navy on white. The only saturated things on screen are the three verdict colors and the expressive blocks behind onboarding and empty states.

Typeset, don't draw

Illustration is one line of type on a color block: round brackets for the head, one letter from a world script for the face. No image assets, no mascots, nothing to redraw when a string changes.

02

Color

Every value is one fixed OKLCH literal in DSColor.swift; the hex below and the contrast table are generated from it. The app is light only: canvas and surface are both pure white so a screenshot is never tinted.

Verdicts — main / on / soft

TRASH
on
soft
Swipe left · the app's own Trash main oklch(0.65 0.19 38) · #eb5927 on #ffffff · soft oklch(0.93 0.036 38) · #ffe0d7
ARCHIVE
on
soft
Swipe right · the Photos album main oklch(0.5 0.24 272) · #3d44e8 on #ffffff · soft oklch(0.93 0.029 272) · #e1e7fc
FAVE
on
soft
Swipe up · the Photos heart main oklch(0.85 0.16 92) · #f3c935 on #1f1e38 · soft oklch(0.95 0.07 95) · #fdefba
The yellow exemption (ADR-004). Golden yellow cannot reach 3:1 against white (it measures 1.58) without turning ochre, so its edge never carries meaning. The navy glyph and the word FAVE do, at 10.20:1 — and every verdict is also a direction and a word. Both fave.main edge rows in the table below print as informational rather than as a failure.

Ink

ink
Primary text, icons#1f1e38 · 16.16 on white
ink2
Secondary text, captions#535366 · 7.49 on white
inkMuted
Placeholder, disabled meta#656571 · 5.77 on white
onToast
Text on the toast capsule#ffffff · 16.16 on toast
onImage
Text on the viewer's black bars#ffffff · 21.00 on black

Surfaces and accent

canvas
Screen background#ffffff
surface
Cards, sheets, letterbox#ffffff
surfaceRaised
Secondary button, list rows#e9e9e9
toast
Toast capsule = navy ink#1f1e38
chip
Date chip over a screenshotwhite α .90 · 12.89 over black

accent / onAccent

The accent is the ink. Primary buttons are navy pills with white labels. Saturation in the chrome is limited to four places: the three stamps, the three verdict buttons, the Trash count badge, and the destructive fill (ADR-005). A color block is not chrome.

focus

Ink at 60 %, 3 pt, outside the shape. The only line the system draws. It was 45 % until that composite measured 2.79:1 on canvas, under the 3:1 non-text bar.

Focused control

scrim · shadowNear · shadowFar

Scrim dims what is behind a sheet at 35 %. The two shadow tokens compose --shadow-1 and --shadow-stack — nothing else casts.

Expressive palette — blocks only, never verdicts

mint

Trash empty

#7cd2ae

violet

Permission denied · white ink

#5849b2

lime

Queue finished

#e0fc41

pink

Favorites empty

#f6c1e9

lavender

Archive empty

#b98cea

Two blocks borrow from elsewhere: onboarding is trash.main (the reference hero is tomato, and onboarding precedes any verdict) and the limited-access interstitial is fave.soft. Nothing runs the other way — an expressive color never means a verdict.

Measured contrast — 43 pairs, 0 failures

#ForegroundBackgroundUseMinRatioResult
1inkcanvastext4.516.16PASS
2inksurfacetext4.516.16PASS
3inksurfaceRaisedtext4.513.35PASS
4ink2canvastext4.57.49PASS
5ink2surfacetext4.57.49PASS
6ink2surfaceRaisedtext4.56.19PASS
7inkMutedcanvastext4.55.77PASS
8inkMutedsurfacetext4.55.77PASS
9inkMutedsurfaceRaisedtext4.54.77PASS
10onToasttoasttext4.516.16PASS
11onAccentaccenttext · primary button label4.516.16PASS
12onImageviewerBackdroptext · viewer bars4.521.00PASS
13inkchip over whitetext · chip over a white screenshot4.516.16PASS
14inkchip over blacktext · chip over a black screenshot4.512.89PASS
15inksurfaceRaisednon-text · secondary button glyph313.35PASS
16focuscanvas over canvasnon-text · focus ring on canvas34.34PASS
17focussurfaceRaised over surfaceRaisednon-text · focus ring on a raised control34.06PASS
18accentcanvasnon-text · primary button edge on canvas316.16PASS
19onAccentinktext · CTA pill label on a block, the pill being the block's ink4.516.16PASS
20accentonAccenttext · CTA pill label on the denied block, the pill being white4.516.16PASS
21ink2canvasnon-text · chrome icons37.49PASS
22trash.ontrash.mainlarge text · stamp word (32 pt) and button glyph on main33.51PASS
23inktrash.softtext · chip on soft4.513.01PASS
24archive.onarchive.mainlarge text · stamp word (32 pt) and button glyph on main36.57PASS
25inkarchive.softtext · chip on soft4.513.12PASS
26fave.onfave.mainlarge text · stamp word (32 pt) and button glyph on main310.20PASS
27inkfave.softtext · chip on soft4.514.01PASS
28trash.maincanvasnon-text · button / badge edge on canvas33.51PASS
29trash.mainwhitenon-text · stamp pill on a white screenshot33.51PASS
30archive.maincanvasnon-text · button / badge edge on canvas36.57PASS
31archive.mainwhitenon-text · stamp pill on a white screenshot36.57PASS
32fave.maincanvasinformational · yellow edge is exempt, the navy word carries meaning (ADR-004)—1.58info
33fave.mainwhiteinformational · same exemption on a white screenshot—1.58info
34inktrash.maintext · destructive button label (16 pt bold) and onboarding copy on tomato4.54.61PASS
35archive.onarchive.maintext · white label on a cobalt fill4.56.57PASS
36inkfave.maintext · copy on a yellow block4.510.20PASS
37inkminttext · copy on the mint block4.59.02PASS
38inklimetext · copy on the lime block4.513.99PASS
39inkpinktext · copy on the pink block4.510.56PASS
40inklavendertext · copy on the lavender block4.56.18PASS
41onAccentviolettext · white copy on the violet block4.56.95PASS
42onAccenttrash.mainlarge text · white stamp word on tomato — never a button label33.51PASS
43surfaceRaisedcanvasinformational · raised fill vs canvas (tint separation, no border)—1.21info
03

Typography

Two voices. Forager Bold Overlap is the display face, at its single weight of 700 — wordmark, display, headline, title and stamp all sit on it. Pretendard is the text face — Regular, Medium, SemiBold, Bold. Eleven roles, mirroring DSTextRole. Ten bind to a Dynamic Type style through relativeTo:; stamp is the exception, returning a fixed-size font so its 44 pt cap cannot be scaled twice, with the call site owning @ScaledMetric.

displayForager Bold Overlap · 48 / 34 · tracking +3 %
Face captions
headlineForager Bold Overlap · 32 / 28
Sift your screenshots.
titleForager Bold Overlap · 26 / 24
Trash · 27 items
counterPretendard Bold 22 · tabular
12 / 340
subheadPretendard SemiBold · 22 / 18
Delete 27 screenshots permanently?
bodyPretendard Regular · 18 / 16 · line-height 1.4
They leave Sift for good and go to Photos' Recently Deleted for 30 days. iOS will double-check — that's expected.
labelPretendard Bold 16
Get started
labelSmallPretendard SemiBold 14
Favorites (12)
captionPretendard Medium · 16 / 14 · tracking +1 %
Sep 21 · 2:14 PM · 1.2 MB
buttonCaptionPretendard Bold 12 · verdict row only · uppercased by .verdict-cap, the string itself is title case
Trash · Fave · Archive
stampForager Bold Overlap 32 · cap 44 · uppercase · tracking +.06em
Archive

The license note

Forager (Mark Simonson Studio) ships through Adobe Fonts, whose license covers web and desktop but not embedding in a mobile app. Until an app license is bought, the app declares the face and resolves it to Pretendard Bold at runtime — DSFont.display probes UIFont(name:"Forager-BoldOverlap") once per cut. This page loads it from Typekit kit blt4ith (family forager-overlap), which carries 400 and 700 — there is no heavier cut, so every display role is set at 700 rather than the 900 the previous face allowed. Without the kit the page renders the same Pretendard fallback, so nothing silently changes shape (ADR-003).

Caption is never a rung above body

Hierarchy moves size and weight in the same direction. A smaller-but-bolder line reads as no hierarchy at all, and at accessibility text sizes the two styles converge — so caption stays under body, never over it. Data digits (counter, sizes, dates, D-numbers) are tabular so the glyphs never jitter.

13 / 340

04

Spacing & Radius

Spacing is 6 · 8 · 12 · 16 · 24 · 28 · 32, inherited verbatim from the previous system (ADR-018). Radius is not a fixed decoration: it tracks the element's inner padding.

Spacing scale — DSSpace

s1 · 6Icon–text gap, inside chips
s2 · 8Small gaps, mobile gutter
s3 · 12Input / button inner padding
s4 · 16Default gap, mobile margin
s5 · 24Card padding, desktop gutter
s6 · 28Wide padding
s7 · 32Section separation

Radius — DSRadius

xs · 6Chips, badges, thumbnails
sm · 8Secondary controls
md · 12Buttons, inputs, segments
lg · 16Cards
xl · 24The screenshot card, sheets
pill · 999Stamps, verdict buttons, toast, CTA pills
Nested radius = outer − gap. A thumbnail (xs 6) sitting 4 pt inside a card corner of md 12 looks wrong at 12 and right at 8. The screenshot card is xl 24 because its inner padding is 24; the stamp on it is a pill because its own padding is 8×16. Radius follows padding, not taste.
05

Icons

Twelve glyphs, bold and solid, sourced from the Noun Project on the free plan. They are template images that inherit currentColor; there is never a filled tile behind a glyph. Until an asset lands in the catalog, DSIconRef.image falls back to the listed SF Symbol — development scaffolding, not a shipping state (ADR-017).

trashtrash solid
archivearchive box solid
faveheart solid
rewindundo arrow bold
restorerestore arrow bold
libraryphoto stack solid
settingsgear solid
closeclose x bold
warningwarning triangle solid
checkcheck circle solid
chevronchevron right bold
photoAccessphoto permission solid

Credits are not optional

The Noun Project's free plan licenses icons under CC BY, so every shipped glyph owes a credit line. The app carries a credits screen fed by DSIconCredits.entries, reachable from the onboarding footer; while that array is empty the screen says no third-party icons ship yet. One entry per downloaded icon, in exactly this shape:

<title> by <author> from Noun Project

The twelve glyphs above are placeholders drawn to the same bold-solid brief so the layouts are real; each one is replaced by its Noun Project export plus its credit line before the app ships.

Do — glyph on the surface

Emphasis comes from the glyph's own color, size and surrounding space. The icon sits directly on the surface.

Don't — filled icon tile

No colored square or rounded tile behind a glyph. A tile invents a surface the system does not have and competes with the verdict colors.

06

Elevation & Layering

Borders are forbidden; the focus ring is the only line the system draws. Surfaces separate by tint and by whitespace. Shadows exist only for things that float — the front card, a committed stamp, the verdict buttons, the toast, sheets — and are two soft layers at 10 % and 5 % of the ink.

Flat — the default

No shadow. This card separates from the panel behind it by tint alone, which is how every list row, chip and segment works.

Floating — --shadow-1

0 1px 3px shadowNear + 0 10px 24px -6px shadowFar, cast downward. DSShadow.floating.

Stacked — --shadow-stack

The same two layers cast upward, so a card reads as lying on the one below it. DSShadow.stack.

In SwiftUI every shadow goes through .dsShadow(_:), which applies compositingGroup() first — otherwise each child is shadowed separately and a card grows a halo.

Layering — DSLayer

baseThe card deck and everything at rest0
stickySticky header, docked verdict row100
dropdownPopovers, context menus1000
modalSheets and their scrim2000
toastToast — always on top3000
07

Interaction & Motion

Only transform and opacity animate; transition: all is banned. Entries ease out, exits are faster, and every animation is gated on Reduce Motion through DSMotion.gated(_:reduce:). Press is scale(0.90) on every button — press one below.

Buttons

Primary is a navy pill with a white label — the accent is the ink. Secondary is surfaceRaised with ink. Destructive is tomato with a navy label at 4.61:1. A 16 pt bold label is not WCAG large text, which starts at 19 pt bold, so white on tomato at 3.51 would not clear the 4.5 bar. It always carries the number. Tab to any of them for the ink focus ring.

On a block the CTA is the block's own ink:
On violet, the one block whose ink is white, it inverts:

The pill is never a free choice between navy and white. Filled with the block’s own ink it clears 4.61 against tomato at worst and 14.01 against pale yellow at best, and its label clears 16.16 either way. The retired wording would have put a white pill on lime at 1.15:1 and a navy pill on violet at 2.33:1 — invisible as shapes (ADR-023).

Chips on soft tints

Trash 27 Archive 58 Favorites 12

Text on a soft tint is always ink, never main — a verdict color on its own tint is the one combination the table does not clear.

Verdict buttons — the gesture's second route

Solid main circles with the on glyph — 64 pt for the two side verdicts, 56 pt for Fave raised 8 pt, so the row reads as a direction map rather than a toolbar. Rewind is a 44 pt ghost pinned to the leading edge and drops to 45 % opacity when there is no history. Tapping runs the same exit pipeline as a swipe.

Motion tokens

0.12 s
dur1 — micro: press, focus
0.18 s
dur2 — standard: state and color changes, the stamp fade on rewind
0.24 s
dur3 — surfaces: sheet, card expand
0.28 s
buttonFlick — the synthesized flick behind a verdict button
0.18 s
exitMin — a hard flick leaves this fast
0.32 s
exitMax — a threshold crawl takes this long
0.18 s
fade — next-card fade-in, cell removal, the Reduce-Motion exit
0.25 s
stampHoldReduced — Reduce Motion holds the stamp this long first

Curves: enter cubic-bezier(.23, 1, .32, 1) · exit cubic-bezier(.32, 0, .67, 0) · standard cubic-bezier(.40, 0, .20, 1). Springs: snap-back 0.35/0.70, land 0.45/0.78, stamp pop 0.28/0.50, press 0.25/0.55. The throw's length is not a constant — it is clamp(720 / |v|, 0.18, 0.32), so momentum is never cut.

Toast and the destructive ladder

Toast

Navy capsule, white text, top-anchored, no action button — the only undo is rewind. It auto-dismisses after 2.5 s and always sits at the top of the z ladder.

Not deleted

Delete 27 screenshots permanently?

They leave Sift for good and go to Photos' Recently Deleted for 30 days. iOS will double-check — that's expected.

Rung 0 (verdicts, viewer trash, library moves, restore) is reversible and asks nothing. Rung 1 (one item, permanently) is confirmed by the iOS dialog alone. Rung 2 (everything) adds the sheet above, which names the count and the size and pre-announces the second dialog, then hands over to iOS. The scrim behind it is ink at 35 %.

Haptics — DSHaptic

EventFeedbackWhy
thresholdArmed.selectionRising edge only, the moment releasing would commit
trash.impact(weight: .heavy)A thud into the bin — the heaviest of the three
archive.impact(weight: .medium)Weight encodes direction, so eyes-off swiping still confirms
fave.impact(weight: .light) × 2, 90 ms apartA heartbeat; the only repeating pulse in the vocabulary
rewind.impact(weight: .light)Undo is a verdict too, just a quiet one
restore.successSomething came back intact
purgeArmed.warningFired just before the iOS delete dialog appears
purgeDone.successThe deletion actually went through
queueDone.successInbox zero
permissionGranted.successThe library opened
segment.selectionFavorites / Archive switch
error.errorNothing else in the system is red, so this carries it

Snap-back and the down dead zone are silent — a non-event should not buzz. There is no sound in v1: with no settings screen, a sound that cannot be turned off would be a defect.

08

Swipe Deck

The product, running. This deck implements DSSwipe and DSMotion exactly as the app will: four 90° sectors with 6° of hysteresis, a commit at 120 pt or 800 pt/s, a velocity-derived throw, and promotion at 60 % of the exit. Drag a card, press a verdict button, or focus the phone and use ← → ↑ and Z.

12 / 340

What the gesture says

←TRASH — the app's own Trash. Reversible until you empty it.
→ARCHIVE — a Photos album you can find later.
↑FAVE — the Photos heart, set on the asset itself.
↓Dead zone — the card follows at 0.35 and springs back. Down means nothing on purpose.

Stamps are pills: TRASH top-right at +12°, ARCHIVE top-left at −12°, FAVE centred at 0°, each with the verdict glyph and the word in the display face. Their opacity ramp ends exactly at the commit distance, so a fully opaque stamp is the "release will commit" signal. Solid fill, not outline: screenshots are usually white and an outline vanishes — the fourth placeholder in this deck is pure white to prove it, and the fifth is near-black.

With Reduce Motion on, the throw is replaced by a cross-fade: the stamp holds for DSMotion.stampHoldReduced so the verdict is readable, then the card fades out over DSMotion.crossFade. Drag still moves the card one-to-one — direct manipulation is not decoration.

Sectors — θ = atan2(−dy, dx)

ARCHIVE FAVE TRASH DEAD 45° 135° 225° 315°

A sector only changes when θ crosses 6° into the neighbour, so a finger resting on a boundary cannot flicker between two verdicts.

The numbers — DSSwipe and the stack

TokenValueWhat it decides
commitDistance120 ptTravel past which release commits. About a third of the card.
commitVelocity800 pt/sVelocity along the sector axis that commits on its own — a deliberate flick.
minTravelForVelocity48 ptFloor under the velocity rule, so a fast tap-drag cannot throw a card.
rotationDivisor20Rotation = dx / 20, anchored near the bottom of the card.
maxRotationDegrees12°Clamp on that rotation in both directions.
stampRevealStart20 ptWhere the stamp starts to appear — after the slop, not before.
stampRevealEnd120 ptFull opacity, deliberately equal to commitDistance.
sectorHysteresisDegrees6°How far into a neighbour θ must cross before the sector flips.
exitDistanceX720 ptHorizontal throw distance for TRASH and ARCHIVE.
exitDistanceY840 ptVertical throw distance for FAVE.
exitRotationDegrees18°Rotation the card lands on as it leaves.
downFollow0.35Rubber-band factor in the dead zone. Never commits.
upScale1.03How far a FAVE drag lifts the card by the commit distance.
promoteAt0.6Fraction of the exit at which the side effect fires and the next card is promoted.
stackOffsetY · stackScaleStep · stackDepth14 pt · 0.06 · 3Cards behind sit at scale 0.94 / 0.88 and y +14 / +28; three are alive at a time. The second card interpolates toward the front as the drag progresses.
exitDuration(velocity:)0.18–0.32 sclamp(720 / |v|, exitMin, exitMax) — a hard flick leaves in 0.18 s, a threshold crawl in 0.32 s.
09

Screens

Four screens, and that is the whole app: Permission, Review, Trash, Library. No settings, no statistics, no AI. Review is the root; Trash and Library are icon buttons in its header that push onto a navigation stack — a tab bar would take 83 pt away from the card and imply three equal destinations.

(ᐛ)

Sift your screenshots.

← TRASH
→ ARCHIVE
↑ FAVE

Get started

Icons

PermissionTomato block, typographic face, navy pill. The library is requested on tap, never on launch.

12 / 340
Sep 21 · 2:14 PM · 1.2 MB

ReviewCounter, two chrome icons, the card, the verdict row. Everything else is the screenshot.

Trash · 27 items · 48 MB
Delete all (27)

TrashA three-column holding pen. One item or all — no multi-select. Both routes end in the iOS dialog.

Favorites (12)Archive (58)

LibraryTwo segments over the same grid. Favorites is the Photos heart; Archive is the app's own list, mirrored to an album the app creates.

Sep 21 · 2:14 PM · 1.2 MB The viewer is the one place the canvas goes black — letterboxing a screenshot on white would tint it. Bars carry onImage text. ArchiveTrash

Color blocks — DSBlock and DSFace

(ஶ)Inbox zero, screenshot edition.
(ツ)Trash is empty. Squeaky.
(ω)No faves yet. Swipe up on the good ones.
(ఠ)Nothing archived. Swipe right to stash keepers.
(ಥ)Sift can't see your screenshots yet.
(ѹ)You picked 14 screenshots. Sift only sees those.

A face is one line: a pair of round brackets in the display face for the head, and a single letter borrowed from a world script doing the eyes and the mouth at once. Seven blocks, seven scripts, no letter twice — Naskapi, Tamil, Katakana, Greek, Telugu, Kannada and Cyrillic, each listed with its code point in DSFace.origin. Only the brackets are Forager; every centre glyph is drawn by the system font that carries its script, and that weight contrast is the intended look. A face is decorative and hidden from VoiceOver; the headline under it carries the meaning. Violet is the only block that takes white ink, at 6.95:1. Six blocks, one per DSBlock case besides onboarding: the limited-access interstitial borrows fave.soft, the second of the two exceptions ADR-016 records.

10

Grid & Responsive

One breakpoint, 1200 px. Above it, the desktop grid: 14 columns where the two ends are fluid margins and content lives in 2–13, gutter 24. Below it, the mobile grid: 4 columns, margin 16, gutter 8. The app only ever uses the mobile grid; this guide is the thing that uses the desktop one.

Desktop — 14 columns, ends are margins

M
2
3
4
5
6
7
8
9
10
11
12
13
M

Mobile — 4 columns, margin 16, gutter 8

1
2
3
4
Drag this window under 1200 px. The sidebar collapses to its brand bar, the type roles drop to their mobile sizes (display 48→34, headline 32→28, body 18→16), and every card grid folds — one breakpoint moves all three at once, which is the point of having only one.
11

Checklist

Run this before a screen leaves your hands.

  • Every color comes from a token — no literal hex outside DesignSystem/, and no opacity used to invent a text color.
  • On a verdict main fill only the 32 pt stamp word and the glyphs use on; every smaller label is ink, and text on a soft tint is ink.
  • Nothing depends on the edge of fave.main — the yellow exemption is acknowledged wherever yellow appears.
  • No borders anywhere. The focus ring is the only line, and surfaces separate by tint and whitespace.
  • Shadows appear only on things that float — front card, committed stamp, verdict buttons, toast, sheets — and go through .dsShadow(_:).
  • Radius tracks the element's inner padding, and a nested radius is the outer radius minus the gap.
  • Type uses only the eleven DSTextRole roles — Forager Bold Overlap for display, Pretendard for text — and caption never sits above body.
  • Every hit target is at least 44 pt; the hit area is padded, never the glyph.
  • Every animation is gated on Reduce Motion through DSMotion.gated, and only transform and opacity animate.
  • VoiceOver reads the front card as one element, with custom actions in the order Trash → Archive → Favorite → Rewind last.
  • Every shipped Noun Project glyph has its CC BY line in DSIconCredits.entries, and no glyph sits inside a filled tile.
  • All four verification commands are green: ds_tokens.py contrast, ds_tokens.py emit-css, ds_tokens.py lint, typecheck-ds.sh.