Skip to content

Instantly share code, notes, and snippets.

@Karthik-B-06
Created May 7, 2026 10:58
Show Gist options
  • Select an option

  • Save Karthik-B-06/cb0bddae226e753e1e8a480e46f52898 to your computer and use it in GitHub Desktop.

Select an option

Save Karthik-B-06/cb0bddae226e753e1e8a480e46f52898 to your computer and use it in GitHub Desktop.

Typography System — Design Doc

Status: Proposal. No code changed yet. Goal: Replace 40+ cryptic tokens with 12 semantic variants and one clean component.


1. What's Wrong Today

Problem Impact
40+ tokens, most duplicates body, body-1, body-6, body-subtext are all 17px/23lh — same style, 4 names
Names carry no meaning subhead-800 doesn't tell you when to use it
Inline overrides negate the token <Typography type="body-7" style={{ fontSize: 12 }}> — why use a token at all?
14px (242 uses) has no token The single most-common inline size is completely unrepresented
20px (41 uses) has no token Same gap
Color always applied inline style={{ color: themeColors.Text_neutralMidHigh }} — repeated in every callsite
Animation baked in isAnimate lives in a text primitive where it doesn't belong

2. Design Decisions

One font, one weight — everywhere

Every existing token already uses TimelessSans-GroteskSemibold. The only exceptions are the two rupee variants (title-800-rupee, subhead-1-rupee) which use Inter-Semibold for currency symbol rendering. The new system makes this explicit: one font constant, no weight mixing.

Semantic names

Names describe what the text is, not how it was built:

display  →  hero numbers, large fares
title1   →  screen-level heading
title2   →  section heading in a scrollable list
title3   →  card heading, prominent list item
subhead1 →  callout label, prominent secondary heading
subhead2 →  navigation text, secondary section label
body1    →  primary readable prose
body2    →  supporting text, ride metadata
label1   →  form labels, button text, list items
label2   →  tags, chips, badge text
caption  →  sub-metadata, timestamp, ride detail line
micro    →  SVG annotations, pass card tiny text

Color via prop

Color is the single most common inline override. A color prop removes 90% of style={{}} usage.

No animation in the primitive

Animation is a composition concern. The caller wraps with Reanimated's Animated.createAnimatedComponent when needed.

Rupee is a prop

One rupee boolean swaps fontFamily to Inter-Semibold for currency symbol rendering. No separate token needed.


3. The New Scale

All values are iOS. Android values are in the second table.

Variant fontSize lineHeight letterSpacing Replaces (old tokens)
display 43 45 0.5 body-4
title1 23 29 0 title-800
title2 20 26 0 (no token — 41 inline uses)
title3 19 27 0 subhead-800, subhead-700, subhead-600, title-3, subhead-4
subhead1 18 25 0.3 callout, callout-1, subhead, subhead-1, body-2
subhead2 16 22 0.2 subhead-2, body-3
body1 17 23 0.2 body, body-1, body-6, body-subtext, body-8*
body2 15 19 0.2 body-7, sub-body-700, sub-body-800, sub-body-500, subhead-3
label1 14 20 0.1 (no token — 242 inline uses)
label2 13 15 0.5 micro, body-5
caption 12 16 0.2 (no token — 85 inline uses)
micro 10 13 0.5 (no token — 19 inline uses)

*body-8 is 19px/23lh — one size step above body1. Absorbing it into body1 is lossy. Verify the 4 callsites before migrating; they may need title3 instead.

Android values (divide iOS by ~1.09)

Variant iOS Android
display 43 39
title1 23 21
title2 20 18
title3 19 17
subhead1 18 16
subhead2 16 15
body1 17 16
body2 15 14
label1 14 13
label2 13 12
caption 12 11
micro 10 9

The existing Android tokens apply ~1.09× correction (Android text renders physically larger at the same point value). Keep this in the SCALE constant with Platform.select, not in individual callsites.


4. Component Draft

Proposed location: consumer/src-v2/primitives/Text.tsx

import React from 'react';
import { Text as RNText, StyleSheet, Platform } from 'react-native';
import type { TextStyle, StyleProp } from 'react-native';
import type { AccessibilityRole } from 'react-native/Libraries/Components/View/ViewAccessibility';
import type { PropsWithChildren } from 'react';

// ─── Variant type ────────────────────────────────────────────────────────────

export type TextVariant =
  | 'display'   // 43px — hero numbers, large fare amounts
  | 'title1'    // 23px — screen headings
  | 'title2'    // 20px — section headings in scroll
  | 'title3'    // 19px — card/list headings
  | 'subhead1'  // 18px — callout labels, prominent secondary headings
  | 'subhead2'  // 16px — navigation text, secondary labels
  | 'body1'     // 17px — primary prose
  | 'body2'     // 15px — supporting text, metadata
  | 'label1'    // 14px — form labels, button text, list items
  | 'label2'    // 13px — chips, badges, tags
  | 'caption'   // 12px — sub-metadata, timestamps
  | 'micro';    // 10px — SVG annotations, pass card tiny text

// ─── Props ───────────────────────────────────────────────────────────────────

type Props = PropsWithChildren<{
  variant: TextVariant;
  color?: string;
  align?: 'left' | 'center' | 'right';
  numberOfLines?: number;
  /** True when displaying a rupee/currency symbol — swaps fontFamily to Inter-Semibold */
  rupee?: boolean;
  style?: StyleProp<TextStyle>;
  accessible?: boolean;
  accessibilityLabel?: string;
  accessibilityRole?: AccessibilityRole;
  testID?: string;
}>;

// ─── Font constants ───────────────────────────────────────────────────────────

const FONT = 'TimelessSans-GroteskSemibold';
const FONT_RUPEE = 'Inter-Semibold';

// ─── Scale ────────────────────────────────────────────────────────────────────

type ScaleValue = Pick<TextStyle, 'fontSize' | 'lineHeight' | 'letterSpacing'>;

const IOS_SCALE: Record<TextVariant, ScaleValue> = {
  display:  { fontSize: 43, lineHeight: 45, letterSpacing: 0.5 },
  title1:   { fontSize: 23, lineHeight: 29, letterSpacing: 0 },
  title2:   { fontSize: 20, lineHeight: 26, letterSpacing: 0 },
  title3:   { fontSize: 19, lineHeight: 27, letterSpacing: 0 },
  subhead1: { fontSize: 18, lineHeight: 25, letterSpacing: 0.3 },
  subhead2: { fontSize: 16, lineHeight: 22, letterSpacing: 0.2 },
  body1:    { fontSize: 17, lineHeight: 23, letterSpacing: 0.2 },
  body2:    { fontSize: 15, lineHeight: 19, letterSpacing: 0.2 },
  label1:   { fontSize: 14, lineHeight: 20, letterSpacing: 0.1 },
  label2:   { fontSize: 13, lineHeight: 15, letterSpacing: 0.5 },
  caption:  { fontSize: 12, lineHeight: 16, letterSpacing: 0.2 },
  micro:    { fontSize: 10, lineHeight: 13, letterSpacing: 0.5 },
};

const ANDROID_SCALE: Record<TextVariant, ScaleValue> = {
  display:  { fontSize: 39, lineHeight: 41, letterSpacing: 0.5 },
  title1:   { fontSize: 21, lineHeight: 27, letterSpacing: 0 },
  title2:   { fontSize: 18, lineHeight: 24, letterSpacing: 0 },
  title3:   { fontSize: 17, lineHeight: 25, letterSpacing: 0 },
  subhead1: { fontSize: 16, lineHeight: 23, letterSpacing: 0.3 },
  subhead2: { fontSize: 15, lineHeight: 20, letterSpacing: 0.2 },
  body1:    { fontSize: 16, lineHeight: 21, letterSpacing: 0.2 },
  body2:    { fontSize: 14, lineHeight: 17, letterSpacing: 0.2 },
  label1:   { fontSize: 13, lineHeight: 18, letterSpacing: 0.1 },
  label2:   { fontSize: 12, lineHeight: 14, letterSpacing: 0.5 },
  caption:  { fontSize: 11, lineHeight: 14, letterSpacing: 0.2 },
  micro:    { fontSize: 9,  lineHeight: 12, letterSpacing: 0.5 },
};

const SCALE = Platform.select({ android: ANDROID_SCALE, default: IOS_SCALE });

// ─── Component ────────────────────────────────────────────────────────────────

export const Text = ({
  children,
  variant,
  color = '#000000',
  align = 'left',
  numberOfLines,
  rupee = false,
  style,
  accessible = true,
  accessibilityLabel,
  accessibilityRole,
  testID,
}: Props) => (
  <RNText
    style={[
      styles.base,
      { fontFamily: rupee ? FONT_RUPEE : FONT },
      SCALE[variant],
      { color, textAlign: align },
      style,
    ]}
    numberOfLines={numberOfLines}
    accessible={accessible}
    accessibilityLabel={accessibilityLabel}
    accessibilityRole={accessibilityRole}
    testID={testID}
  >
    {children}
  </RNText>
);

const styles = StyleSheet.create({
  base: {
    fontFamily: FONT,
    color: '#000000',
  },
});

5. Usage — Before & After

Screen heading

// Before
<Typography type="title-800" style={{ color: themeColors.Text_highContrast }}>
  Where to?
</Typography>

// After
<Text variant="title1" color={themeColors.Text_highContrast}>
  Where to?
</Text>

Ride metadata row

// Before
<Typography
  type="body-7"
  style={{ color: themeColors.Text_neutralMidHigh, fontSize: 12 }}
>
  3.2 km · 12 min
</Typography>

// After — fontSize 12 = caption
<Text variant="caption" color={themeColors.Text_neutralMidHigh}>
  3.2 km · 12 min
</Text>

Fare amount

// Before
<Typography type="title-800-rupee" style={{ color: themeColors.Text_highContrast }}>
  ₹249
</Typography>

// After
<Text variant="title1" rupee color={themeColors.Text_highContrast}>
  ₹249
</Text>

Chip / tag label

// Before
<Text style={{ fontSize: 12, fontFamily: 'TimelessSans-GroteskSemibold', color: '#767676' }}>
  AC
</Text>

// After
<Text variant="label2" color="#767676">
  AC
</Text>

Animated text (composed at callsite)

import Animated from 'react-native-reanimated';
import { Text } from '@/src-v2/primitives/Text';

const AnimatedText = Animated.createAnimatedComponent(Text);

// Use AnimatedText exactly like Text, plus animatedProps

6. Migration Map — Old Token → New Variant

Old type="…" New variant="…" Notes
title-800 title1 Direct swap
title-2, title-4 title1 Only 1 use each — verify visually
subhead-900 title1 21px → 23px, slightly larger. 1 use
subhead-800, subhead-700, subhead-600 title3 Line height unifies to 27
title-3, subhead-4 title3 Direct swap
callout, callout-1, callout-2 subhead1 Direct swap
subhead, subhead-1 subhead1 Direct swap
body-2 subhead1 18px → same size, slight lh change (23→25)
subhead-2 subhead2 Direct swap
body-3 subhead2 16px, tight lh — verify 6 callsites
body, body-1, body-6 body1 Direct swap
body-subtext body1 Direct swap
body-8 title3 or body1 19px — check 4 callsites for intent
body-7, sub-body-700, sub-body-800, sub-body-500 body2 Direct swap
subhead-3 body2 15px, slightly tighter lh (17→19)
body-5 label2 13px — direct swap
micro label2 13px — direct swap
body-4 display 43px — verify it's still used
title-800-rupee title1 + rupee prop
subhead-1-rupee subhead2 + rupee prop 16px matches subhead2

Inline overrides to migrate:

Inline fontSize Use variant
20, 21 title2
18, 19 title3 or subhead1 — check context
16, 17 subhead2 or body1
14, 15 label1 or body2
12, 13 caption or label2
10, 11 micro

7. One-Off Font Sizes — Designer Review

These sizes fall outside the 12-variant scale. Every one of them is a hardcoded fontSize with no token. Listed here so design can decide: add to the scale, round to the nearest variant, or keep as a deliberate one-off.


24px — 20+ occurrences (most significant gap)

File Context
src/typescript/screens/SafetyModal.tsx Safety modal heading
src/typescript/screens/safetyCenter/UI.tsx (×3) Safety center section headings
src/typescript/screens/safetyCenter/safetySettings/components/TrustedContactsList.tsx Contacts list heading
src/typescript/screens/safetyCenter/safetySettings/components/ManualContactModal.tsx Manual contact modal heading
src/typescript/screens/safetyCenter/safetySettings/components/TrustedContactsHelp.tsx Help screen heading
src/typescript/screens/safety/UI.tsx (×2) Safety screen headings
src/typescript/components/common/OTPComponentNew.tsx (×2) OTP digit display
src/typescript/components/common/OTPComponentNew1.tsx (×2) OTP digit display
src/typescript/components/PromotionalModal.tsx Promo modal heading
src/typescript/components/ny-service/GenericVideoBottomSheet.tsx Bottom sheet heading
src/FallbackUI.tsx Error fallback heading
src-v2/screens/BusinessProfile/UI.tsx (×4) Business profile headings
src-v2/screens/MetroIssueFaq/components/RaiseTicketModal.tsx Ticket modal heading
src-v2/screens/ReportIssueChatScreen/UI.tsx Chat screen heading
src-v2/screens/JourneySimulation/index.tsx (×3) Journey step headings
src-v2/screens/Ticketing/PaymentStatusScreen/components/PaymentSucessView.tsx Payment success heading
src-v2/screens/Ticketing/PaymentStatusScreen/components/PaymentPendingView.tsx Payment pending heading
src-v2/screens/Ticketing/PaymentStatusScreen/components/PaymentFailedView.tsx Payment failed heading
src-v2/screens/Ticketing/MyTicketScreen/UI.tsx Ticket screen heading
src-v2/screens/Ticketing/EventDetailsScreen/UI.tsx Event detail heading
src-v2/screens/reviewAndFeedback/variants/lynx/components/FeedbackSheet.tsx Feedback sheet heading
src-v2/multimodal/screens/BusOtpFlow/OtpComponent.tsx (×2) Bus OTP digit display
src-v2/components/PickupInstructionsModal/AudioRecordingModal.tsx (×4) Recording modal
src-v2/components/PickupInstructionsModal/UI.tsx Pickup instructions heading

Designer call: 24px is one point above title1 (23px). Add it as a named variant (e.g. heading at 24px), or round all uses down to title1?


26px — 4 occurrences

File Context
src/typescript/screens/CallScreen.tsx:318 Caller name display
src-v2/screens/ProfileTab/UI.tsx:328 Profile tab prominent label
src-v2/screens/reviewAndFeedback/variants/lynx/components/RideSummaryHeader.tsx:114 Ride summary header
src-v2/screens/Invoice/components/InvoiceCard.tsx:80 Invoice amount (rupee, lineHeight: 27)

Designer call: Round to title1 (23px) or add a variant?


27px — 1 occurrence

File Context
src-v2/multimodal/screens/BusOtpFlow/components/Keypad.tsx:28 Numeric keypad digit

Designer call: Intentional keypad size — likely keep as one-off.


28px — 5 occurrences

File Context
src/typescript/screens/CallScreen.tsx:434 Call screen secondary display
src/typescript/components/ComingSoonModal.tsx:93 "Coming soon" modal heading
src-v2/screens/JourneySimulation/index.tsx:545, 665 Journey step value display
src-v2/components/PickupInstructionsModal/UI.tsx:677 Pickup instructions heading

Designer call: Round to title1 (23px), or keep as one-off for modal hero moments?


30px — 1 occurrence

File Context
src-v2/screens/Referral/UI.tsx:399 Referral reward display

Designer call: Keep as one-off (referral hero number).


32px — 3 occurrences

File Context
src/typescript/designSystem/components/RevisedFareCard.tsx:77 Fare card amount
src/typescript/components/common/AnimatedModal.tsx:261 Animated modal heading
src-v2/components/TippingModal/UI.tsx:250 Tip amount display

Designer call: These are all hero numbers / amounts. Round to display (43px) seems too big — add a mid-display variant, or keep as one-offs?


36px — 1 occurrence

File Context
src-v2/screens/JourneySimulation/index.tsx:590 Journey simulation value

Designer call: Keep as one-off.


40px — 3 occurrences

File Context
src-v2/screens/RecentChatDetail/UI.tsx:332, 347 Emoji / icon character display
src-v2/screens/BusinessProfile/UI.tsx:998 Business profile avatar/initial

Designer call: These are likely emoji or avatar initials, not text. Keep as one-offs.


48px — 3 occurrences

File Context
src/typescript/screens/CallScreen.tsx:313 Call screen avatar / icon
src-v2/screens/BusinessProfile/UI.tsx:1229, 1233 Business profile large display

Designer call: Keep as one-offs — these are icon/avatar sized, not body text.


8. What to Delete After Migration

Token Reason
callout-2 0 uses
body-4 0 uses (confirm, then delete)
subhead-4 0 uses
title-2, title-4 1 use each — absorb into title1
subhead-900 1 use — absorb into title1
All of src/typescript/constants/style.tsx text styles Legacy PlusJakartaSans — retire after migrating old screens

8. Files to Touch (Implementation Order)

  1. Create consumer/src-v2/primitives/Text.tsx — the new component
  2. Update consumer/src-v2/primitives/index.ts — export Text and TextVariant
  3. Migrate high-traffic screens first:
    • src-v2/screens/MyBookingDetails/components/BookingRideDetails.tsx (100+ uses)
    • src-v2/screens/Preferences/PreferenceDetailUI.tsx
    • src-v2/screens/LookingForRides/UI.tsx
    • src-v2/primitives/Dropdown.tsx
    • src-v2/primitives/Snackbar.tsx
  4. Keep src/typescript/designSystem/components/primitives/Typography.tsx alive until all src/typescript/ screens are migrated
  5. Delete old tokens and Typography once 0 references remain

9. Token Files (Reference)

File Purpose
src/typescript/designSystem/tokens/nammaYatriTokens.ts iOS token definitions (to retire)
src/typescript/designSystem/tokens/nammaYatriTokensAndroid.ts Android token definitions (to retire)
src/typescript/designSystem/components/primitives/Typography.tsx Current <Typography> (to retire)
src/typescript/tailwindTheme/componentUtils/primitives/typography.ts Tailwind utils (to retire)
src/typescript/constants/style.tsx Legacy PlusJakartaSans styles (to retire)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment