Accessibility

Settld targets WCAG 2.1 AA compliance as a minimum bar. Most key color pairs exceed AA — several reach AAA. The warm cream palette was chosen partly because it achieves better contrast with ink than cold white does at equivalent visual warmth.

Color contrast

Ink on paper
Body text, buttons
12.5:1
#171513 on #FBF8F0
AAA
Ink on canvas
Page body text
11.8:1
#171513 on #F6F1E6
AAA
Ink-body on canvas
Secondary text
7.2:1
#4A443D on #F6F1E6
AAA
Teal on paper
Links, settle button, success
4.8:1
#0E7C66 on #FBF8F0
AA
Coral on paper
Owe amounts, errors
3.6:1
#E76F51 on #FBF8F0
Decorative only
Paper on teal
Settle button label
4.8:1
#FBF8F0 on #0E7C66
AA
Paper on dark canvas
Dark surface text
13.2:1
#FBF8F0 on #1E1A16
AAA
Muted on canvas
Labels, timestamps, captions
3.5:1
#8A8276 on #F6F1E6
Non-critical only
Coral and ink-mute contrast Coral (#E76F51) and ink-mute (#8A8276) do not pass AA contrast on light surfaces at normal text sizes. This is intentional: both are used in supporting roles (owe amount display, timestamps) where adjacent high-contrast text provides the accessible alternative. Never use coral or muted ink as the sole carrier of critical information without a paired icon or secondary label.

Keyboard navigation

Tab
Move focus forward through interactive elements in DOM order
Shift + Tab
Move focus backward
Enter
Activate button or link; submit form
Space
Activate button; toggle checkbox or switch
Esc
Close modal, sheet, or tooltip; deselect chip
← →
Move between segmented control options; navigate tab bar
↑ ↓
Move through dropdown options and list items
Home / End
Jump to first / last option in a list or dropdown

Tab order follows DOM order. Avoid using tabindex values other than 0 and -1. If the visual order doesn't match the DOM order, fix the DOM — don't patch it with tabindex.

Modals and sheets trap focus: the first focusable element receives focus on open; Tab wraps within the modal; Esc returns focus to the trigger element. Use aria-modal="true" and set the background to inert while the modal is open.

Focus rings

Button focused
Input focused

All interactive elements use a consistent focus ring: outline: 2px solid var(--teal) with outline-offset: 2px. This applies globally via:

:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
}
/* Reset for mouse users (browser default behavior) */
:focus:not(:focus-visible) {
  outline: none;
}
Never remove focus outlines If a design requires invisible focus rings, that's a design problem, not an accessibility trade-off. The teal outline is visible and intentional. If it clashes with a dark surface, the outline becomes paper-colored: outline-color: var(--s-paper) on dark backgrounds.

Screen reader support

Component ARIA pattern Notes
Button <button> with text label Icon-only buttons require aria-label. Loading state adds aria-busy="true" and keeps original label in aria-label.
Segmented control role="radiogroup" with role="radio" children Active segment has aria-checked="true". Label the group: aria-label="Split mode".
Tab bar role="tablist" with role="tab" children Active tab has aria-selected="true". Badge count in aria-label: "Activity, 3 unread".
Modal / sheet role="dialog" with aria-modal="true" Add aria-labelledby pointing to the modal title. Trap focus. Set background to inert.
Person chip role="checkbox" or role="option" Use aria-checked for toggleable chips. In a listbox context, use aria-selected.
Split bar role="img" with aria-label Example: aria-label="Split: You ₹1,400 (33%), Aarav ₹1,400 (33%), Priya ₹1,400 (34%)"
Cap meter role="progressbar" Add aria-valuemin="0", aria-valuemax="50", aria-valuenow="47", aria-label="Monthly expenses: 47 of 50".
Avatar aria-label on container The letter inside the avatar is decorative. Provide aria-label="Aarav's avatar" on the container. Use aria-hidden="true" on the letter.
Toast / notification role="status" or role="alert" Use role="alert" for errors (forces immediate announcement). Use role="status" for confirmations (polite). Never use role="alert" for success states.
Empty state Plain HTML, no special role The glyph is aria-hidden="true". The title and body are semantic heading + paragraph. The action button is a standard <button>.

Motion and animation

prefers-reduced-motion Wrap all non-essential animations in a @media (prefers-reduced-motion: reduce) query. This includes: confetti fall, card tilt on hover, skeleton shimmer, split bar transitions, rolling ledger queue. Essential animations (focus ring, button press scale) are retained even in reduced motion — they're interaction feedback, not decoration.
@media (prefers-reduced-motion: reduce) {
  /* Disable non-essential animations */
  .confetti-particle { animation: none; opacity: 0.3; }
  .split-segment    { transition: none; }
  .ledger-track     { transition: none; }
  .cap-meter-fill   { transition: none; }

  /* Keep essential feedback */
  .btn:active { transform: scale(0.97); } /* Retain — it's tactile feedback */
  :focus-visible { outline: 2px solid var(--teal); } /* Always visible */
}

Touch targets

Tab bar item
44×44 minimum
44
Person chip
36px height min
36
Icon button
40×40 minimum
40
Ledger row
44px height min
44px row

The minimum touch target is 44×44px, per Apple HIG and WCAG 2.5.5. For elements that are visually smaller than 44px (chips at 28px height, icon badges at 20px), expand the tap area using padding or ::after pseudoelement — never the visual bounds.

The ledger row minimum height is 44px. On dense displays where vertical space is tight, never go below 40px — financial data requires tap precision.

/* Expanding tap area without visual change */
.small-chip {
  position: relative;
}
.small-chip::after {
  content: "";
  position: absolute;
  inset: -8px; /* expands tap target by 8px on each side */
}

Text sizing and zoom

All font sizes use px units, which scale with browser text size settings and OS-level font scaling. Never use vw-only fluid type for body text — it breaks when text zoom is applied. Use clamp() only for display sizes (headlines), always with a px minimum that passes at 200% text zoom.

The app must be usable at 200% browser zoom without horizontal scrolling on a 375px viewport. Test with Chrome's DevTools or the browser's built-in zoom. Horizontal overflow is a failure, not an edge case.

The minimum font size for body text is 13px (.body-sm). Never render data labels, captions, or form helper text below 12px. The JetBrains Mono eyebrow labels at 10px are intentionally below this threshold — they are supplementary, never the primary carrier of information.

Text roleMin sizeScale tokenNotes
Body copy15pxbody-mdPrimary reading size
Secondary body14pxbody-sm + 1pxActivity feed, list meta
Captions13pxbody-smTimestamps, helper text
Labels / eyebrows10pxmono-smJetBrains Mono only. Always uppercase. Never primary.
Amount display16pxContext-dependentJetBrains Mono. Financial data must be readable at a glance.