Skip to content

Spacing & Elevation

How we use space (between elements) and depth (between layers). Both compose into the "tonal layering" approach that defines Tomoda's UI.

Spacing scale

A 4-pixel base with named steps. Stick to the scale — half-steps and arbitrary pixels make the system feel sloppy.

xs — 4px
sm — 8px
md — 12px
lg — 16px
xl — 24px
2xl — 32px
3xl — 48px

When to use which

Step Use
xs (4) Icon-to-text gap, inline label-value pairing
sm (8) Tight grouping — within a card, between list metadata
md (12) List item vertical separation
lg (16) Section padding inside a card
xl (24) Card internal padding, top-of-section margin
2xl (32) Between major content blocks
3xl (48) Hero / display surfaces, generous landing spacing

Avoid lg for card internal padding when content is rich — xl or 2xl lets the content breathe and reinforces the "curator's frame" feel.

Border radius

Three corner sizes. Use one consistently per surface tier.

Token Value Use
sm 6px Chips, pills, tight controls
md 10px Buttons, inputs, small cards
lg 16px Cards, modals, sheets
xl 24px Large hero surfaces, full-bleed cards

Don't mix md and lg for elements at the same hierarchy level — the inconsistency reads as accidental.

Icon & logo sizing

Two named scales set the size of glyphs and the brand mark, in px. They live in frontend/constants/icons.ts as ICON_SIZES and LOGO_SIZES, mirroring how SPACING and RADIUS work. Size from a token, not a hardcoded number, so glyphs stay consistent across screens.

ICON_SIZES covers in-app iconography:

Token Value Use
xs 14px Inline affordance next to text (chevron in a label row)
sm 16px Metadata glyph, dense list trailing icon
md 18px Default icon in a row or button
lg 20px Primary action icon, tab bar
xl 22px Emphasised row / control glyph
xxl 24px Prominent header or empty-state icon
xxxl 28px Feature glyph, oversized touch target
display 40px Hero / illustration-scale glyph

LOGO_SIZES sizes the brand T mark for the surfaces it sits on:

Token Value Use
sm 28px Inline header wordmark, compact nav
md 32px Sidebar, landing overlay wordmark
lg 52px Welcome / onboarding brand lockup
xl 64px Auth header brand lockup

Every in-app icon renders through the shared VectorIcon (a bare Ionicons name or a family:name spec) or through LogoT, sized from these tokens. LogoT's size prop takes a LogoSize token (default md) or a raw number for the rare off-scale case. Reach for a raw number only when a value falls outside the scale; otherwise the token keeps the mark in step with the rest of the system.

Surface ladder (depth without shadow)

The core insight: depth comes from stepping the surface ladder, not from shadows. Place a higher-tier surface on top of a lower-tier one and the eye sees real depth. Values are the source of truth in constants/Themes.ts; the per-theme card rung is expressed once in constants/surfaces.ts.

The in-page ladder

Light and dark do not use the same token per layer. In dark, surface (#17171A) recedes below the page background, so it is not a card token there. The card/row layer is per theme: white in light, surfaceContainerHigh in dark, resolved by cardSurface(theme).

Visual layer Dark token / hex Light token / hex
Page background background #1C1C1E background #F7F5F0
Section well (wraps cards) surfaceContainerLow #1E1E22 surfaceContainerLow #F0EDE6
Card / row (cardSurface) surfaceContainerHigh #28282D surface #FFFFFF
Chip / input on a card surfaceContainerHighest #32323A surfaceContainerHighest #D8D3C8
Nested on highest primary @ 1A primary @ 1A

Composition rules

  1. A standalone card or row on the page uses the card rung (cardSurface(theme)), never surfaceContainerLow. Low is near-invisible against the dark page background on its own.
  2. surfaceContainerLow is a well that contains cards, not a standalone panel. It reads as "carved" only when cards sit inside it.
  3. A chip or input sitting on a card steps to surfaceContainerHighest so it separates from the card tone (in dark the card is already surfaceContainerHigh).

surface is the overlay tier

surface is the floating / overlay token (sheets, popovers, modals), not an in-page card token. For an in-page card or row, reach for cardSurface(theme).

Shadows (used sparingly)

Reserved for elements that genuinely float. The surface ladder handles all in-context depth.

Token Value Use
shadow-modal 0px 24px 48px rgba(0, 0, 0, 0.35) Modals, fullscreen sheets
shadow-floating 0px 12px 24px rgba(0, 0, 0, 0.25) Dropdowns, popovers
shadow-elevated 0px 4px 12px rgba(0, 0, 0, 0.20) Sticky bottom bars, FABs

Shadow rules

  • Never pure black. Shadow opacity tops out at 0.4 to keep the warm-charcoal feel.
  • Diffused, not crisp. Large blur radius, no spread. Shadows should feel like ambient occlusion, not hard cutouts.
  • No top shadow. Light always falls from above; shadow always falls below. Top-shadows look like floating UFOs.
  • No shadow on a background surface. If the underlying surface is the app root, you don't need a shadow to imply depth — use the surface ladder.

Glassmorphism

A specific exception to the no-shadow rule: floating UI that should feel frosted rather than opaque.

  • Surface: surfaceContainerLow at 70% opacity
  • Backdrop filter: blur(24px)
  • Wrapped by the GlassView component which handles native (expo-blur) and web (backdrop-filter CSS) paths.

Use for floating navigation pills, contextual overlays on top of imagery, and the chat composer when it sits over content. Don't use as a default — every glass surface is a moment.

Ghost borders

When a border is absolutely required (focus state, error state, accessibility outline), use the border token, never solid:

  • Dark: rgba(255, 255, 255, 0.15)
  • Light: rgba(0, 0, 0, 0.10)

This is the only sanctioned use of a border in product UI. Solid #000 / #fff borders are forbidden — see the no-line rule.

Layout density

Two density modes appear in production:

  • Comfortable (default)xl padding on cards, lg/xl between list items. Used everywhere user-content lives (chat, event detail, profile).
  • Denselg padding, md between items. Used in admin tables, settings lists, anywhere the screen is configuration-heavy.

Don't introduce a third density. Comfortable defaults serve almost everything; dense covers the rest.