diff --git a/packages/design-system-react-native/src/components/TitleSubpage/README.md b/packages/design-system-react-native/src/components/TitleSubpage/README.md
new file mode 100644
index 000000000..033d2543a
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/README.md
@@ -0,0 +1,512 @@
+# TitleSubpage
+
+TitleSubpage lays out a required identity block (leading `titleAvatar` beside a title stack), an optional subtitle, an optional amount row, optional bottom rows, and optional inline accessories per row. On React Native, `titleAvatar` is passed straight through as the identity `BoxRow` `startAccessory` (no fixed-size or `overflow: hidden` wrapper), so network badges and other overlays can extend past the token without being clipped. For the default layout, use `AvatarToken` at `AvatarTokenSize.Lg` (40×40) so the token aligns with the row; the component stays agnostic—you own composition when you need badges or other chrome.
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+/>;
+```
+
+## Props
+
+### `titleAvatar`
+
+Leading visual for the identity row (required). Passed as the `startAccessory` of the identity `BoxRow` with no inner layout wrapper. Prefer `AvatarToken` at `AvatarTokenSize.Lg` for the standard 40×40 footprint; wrap in `BadgeWrapper` (or similar) when you need a network badge or other element that should sit outside the token bounds.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ------- |
+| `ReactNode` | Yes | — |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+/>;
+```
+
+```tsx
+import {
+ AvatarNetwork,
+ AvatarNetworkSize,
+ AvatarToken,
+ AvatarTokenSize,
+ BadgeWrapper,
+ BadgeWrapperPosition,
+ BadgeWrapperPositionAnchorShape,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+// Supply `src` (or equivalent) for your token and network artwork.
+
+
+ }
+ position={BadgeWrapperPosition.BottomRight}
+ positionAnchorShape={BadgeWrapperPositionAnchorShape.Circular}
+ >
+
+
+ }
+ title="USD Coin"
+/>;
+```
+
+### `title`
+
+Title row (required). The row renders when `title` is truthy. When `title` is a string, it uses `TextVariant.HeadingSm` and `TextColor.TextDefault` (merged with `titleProps`). Pass a `ReactNode` for custom layout.
+
+Legacy `TitleStandard` `topLabel` maps to `title` on `TitleSubpage`. The old main-line value (large amount) maps to `amount`, not `title`.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ------- |
+| `ReactNode` | Yes | — |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+ bottomLabel="0.002 ETH"
+/>;
+```
+
+### `titleEndAccessory`
+
+Optional node to the right of `title` in the title row (same pattern as `amountEndAccessory`). Only renders when the title row is shown (truthy `title`).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ Icon,
+ IconName,
+ IconSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ titleEndAccessory={}
+ amount="$4.42"
+/>;
+```
+
+### `subtitle`
+
+Optional subtitle row between the title and the amount. The row renders when `subtitle` is truthy. When `subtitle` is a string, it uses `TextVariant.BodySm`, medium weight, and `TextColor.TextAlternative` (merged with `subtitleProps`). Pass a `ReactNode` for custom layout.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ subtitle="Account 1"
+ amount="$4.42"
+/>;
+```
+
+### `subtitleEndAccessory`
+
+Optional node to the right of `subtitle` in the subtitle row (same pattern as `titleEndAccessory`). Only renders when the subtitle row is shown (truthy `subtitle`).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ Icon,
+ IconName,
+ IconSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ subtitle="Account 1"
+ subtitleEndAccessory={}
+ amount="$4.42"
+/>;
+```
+
+### `amount`
+
+Optional primary amount line below the title and optional subtitle. The amount row renders when `amount` is truthy (for example a non-empty string or a `ReactNode`). Falsy values such as `false`, `null`, `undefined`, or `''` hide the row and do not render `amountEndAccessory`. When `amount` is a string, it is wrapped with display typography (`TextVariant.DisplayLg` and `amountProps`); other `ReactNode` values render as provided.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Balance"
+ amount="$1,234.56"
+/>;
+```
+
+### `amountEndAccessory`
+
+Optional node rendered to the right of the amount (for example an info icon). Only renders when the amount row is shown (truthy `amount`).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ Icon,
+ IconName,
+ IconSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+ amountEndAccessory={}
+/>;
+```
+
+### `bottomLabel`
+
+Optional bottom label row. The row renders when `bottomLabel` is truthy; `bottomLabelEndAccessory` only appears on that row when `bottomLabel` is truthy. When `bottomLabel` is a string, it uses `TextVariant.BodySm`, medium weight, and `TextColor.TextAlternative` (merged with `bottomLabelProps`). When this row is shown, `bottomAccessory` is not rendered.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+ bottomLabel="0.002 ETH"
+/>;
+```
+
+### `bottomLabelEndAccessory`
+
+Optional node to the right of `bottomLabel` in the bottom label row (for example a `Text` label or an icon). The bottom label row only renders when `bottomLabel` is truthy, so this accessory does not appear on its own without `bottomLabel`.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ FontWeight,
+ Text,
+ TextColor,
+ TextVariant,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="USD Coin"
+ subtitle="USDC"
+ amount="$1.0001"
+ bottomLabel="+$0.000126 (+0.01%)"
+ bottomLabelEndAccessory={
+
+ Today
+
+ }
+ bottomLabelProps={{ color: TextColor.SuccessDefault }}
+/>;
+```
+
+### `bottomAccessory`
+
+Optional custom bottom row when `bottomLabel` is omitted or not truthy (for example `false`, `null`, `undefined`, or `''`). Renders without default label typography; compose layout inside the node.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ Box,
+ BoxAlignItems,
+ BoxFlexDirection,
+ Icon,
+ IconName,
+ IconSize,
+ Text,
+ TextVariant,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+ bottomAccessory={
+
+
+ ~$0.50 fee
+
+ }
+/>;
+```
+
+### `amountProps`
+
+Optional props merged into the amount `Text` when `amount` is a string. Use for `testID` or typography overrides.
+
+| TYPE | REQUIRED | DEFAULT |
+| -------------------------------------- | -------- | ----------- |
+| `Omit, 'children'>` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+ amountProps={{ testID: 'title-subpage-amount' }}
+/>;
+```
+
+### `titleProps`
+
+Optional props merged into the title row `Text` when `title` is a string.
+
+| TYPE | REQUIRED | DEFAULT |
+| -------------------------------------- | -------- | ----------- |
+| `Omit, 'children'>` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ titleProps={{ testID: 'title-subpage-title' }}
+ amount="$4.42"
+/>;
+```
+
+### `subtitleProps`
+
+Optional props merged into the subtitle row `Text` when `subtitle` is a string.
+
+| TYPE | REQUIRED | DEFAULT |
+| -------------------------------------- | -------- | ----------- |
+| `Omit, 'children'>` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ subtitle="Account 1"
+ subtitleProps={{ testID: 'title-subpage-subtitle' }}
+ amount="$4.42"
+/>;
+```
+
+### `bottomLabelProps`
+
+Optional props merged into the bottom label `Text` when `bottomLabel` is a string.
+
+| TYPE | REQUIRED | DEFAULT |
+| -------------------------------------- | -------- | ----------- |
+| `Omit, 'children'>` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+}
+ title="Send"
+ amount="$4.42"
+ bottomLabel="0.002 ETH"
+ bottomLabelProps={{ testID: 'title-subpage-bottom' }}
+/>;
+```
+
+### `identityRowProps`
+
+Optional props spread onto the identity `BoxRow` after defaults. `children`, `startAccessory`, and `textProps` are reserved by the component.
+
+| TYPE | REQUIRED | DEFAULT |
+| --------------------------------------------------------------------------- | -------- | ----------- |
+| `Omit, 'children' \| 'startAccessory' \| 'textProps'>` | No | `undefined` |
+
+### `titleColumnProps`
+
+Optional props spread onto the title/subtitle column `Box`. `children` is reserved by the component.
+
+| TYPE | REQUIRED | DEFAULT |
+| ------------------------------------- | -------- | ----------- |
+| `Omit, 'children'>` | No | `undefined` |
+
+### `bottomLabelWrapperProps`
+
+Optional props spread onto the bottom label `BoxRow` after defaults. `children`, `endAccessory`, and `textProps` are reserved by the component.
+
+| TYPE | REQUIRED | DEFAULT |
+| ------------------------------------------------------------------------- | -------- | ----------- |
+| `Omit, 'children' \| 'endAccessory' \| 'textProps'>` | No | `undefined` |
+
+### `twClassName`
+
+Use the `twClassName` prop to add Tailwind CSS classes to the component. These classes will be merged with the component's default classes using `twMerge`, allowing you to:
+
+- Add new styles that don't exist in the default component
+- Override the component's default styles when needed
+
+| TYPE | REQUIRED | DEFAULT |
+| -------- | -------- | ----------- |
+| `string` | No | `undefined` |
+
+```tsx
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+// Add additional styles
+}
+ twClassName="mt-4"
+ title="Send"
+ amount="$4.42"
+/>
+
+// Override default styles
+}
+ twClassName="px-6"
+ title="Send"
+ amount="$4.42"
+/>
+```
+
+### `style`
+
+Use the `style` prop to customize the component's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values.
+
+| TYPE | REQUIRED | DEFAULT |
+| ---------------------- | -------- | ----------- |
+| `StyleProp` | No | `undefined` |
+
+```tsx
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+
+import {
+ AvatarToken,
+ AvatarTokenSize,
+ TitleSubpage,
+} from '@metamask/design-system-react-native';
+
+export const ConditionalExample = ({ isActive }: { isActive: boolean }) => {
+ const tw = useTailwind();
+
+ return (
+ }
+ title="Send"
+ amount="$4.42"
+ style={tw.style('opacity-90', isActive && 'opacity-100')}
+ />
+ );
+};
+```
+
+## References
+
+[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940)
diff --git a/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.stories.tsx b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.stories.tsx
new file mode 100644
index 000000000..5a498f1c2
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.stories.tsx
@@ -0,0 +1,238 @@
+import {
+ FontWeight,
+ TextColor,
+ TextVariant,
+} from '@metamask/design-system-shared';
+import type { Meta, StoryObj } from '@storybook/react-native';
+import React from 'react';
+
+import UsdcSVG from '../../assets/token-icons/usdc.svg';
+import { AvatarToken, AvatarTokenSize } from '../AvatarToken';
+import { Box, BoxAlignItems, BoxFlexDirection } from '../Box';
+import { Icon, IconName, IconSize, IconColor } from '../Icon';
+import { Text } from '../Text';
+
+import { TitleSubpage } from './TitleSubpage';
+import type { TitleSubpageProps } from './TitleSubpage.types';
+
+/**
+ * Token avatar for stories using bundled USDC artwork.
+ *
+ * @returns The USDC `AvatarToken` for story defaults.
+ */
+const StoryTitleAvatar = () => (
+
+);
+
+const USDC_TITLE = 'USD Coin';
+const USDC_SUBTITLE = 'USDC';
+const USDC_AMOUNT = '$1.0001';
+const USDC_PRICE_CHANGE_BOTTOM_LABEL = '+$0.000126 (+0.01%)';
+
+const TodayBottomLabelEndAccessory = () => (
+
+ Today
+
+);
+
+/**
+ * Pill badge: dot + label (e.g. network), for `titleEndAccessory`.
+ * TODO: Temporary until a Tag component exists.
+ *
+ * @returns Story-only testnet badge UI.
+ */
+const TestnetBadge = () => (
+
+
+
+ Testnet
+
+
+);
+
+const meta: Meta = {
+ title: 'Components/TitleSubpage',
+ component: TitleSubpage,
+ args: {
+ titleAvatar: ,
+ title: USDC_TITLE,
+ subtitle: USDC_SUBTITLE,
+ amount: USDC_AMOUNT,
+ twClassName: '',
+ },
+ argTypes: {
+ title: {
+ control: 'text',
+ },
+ amount: {
+ control: 'text',
+ },
+ subtitle: {
+ control: 'text',
+ },
+ bottomLabel: {
+ control: 'text',
+ },
+ twClassName: { control: 'text' },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: (args) => (
+ }
+ bottomLabelProps={{ color: TextColor.SuccessDefault }}
+ />
+ ),
+};
+
+export const Amount: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ amount={USDC_AMOUNT}
+ />
+ ),
+};
+
+export const AmountEndAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ amount={USDC_AMOUNT}
+ amountEndAccessory={
+
+ }
+ />
+ ),
+};
+
+export const Title: Story = {
+ render: () => (
+ } title={USDC_TITLE} />
+ ),
+};
+
+export const TitleEndAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ titleEndAccessory={}
+ />
+ ),
+};
+
+export const Subtitle: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ />
+ ),
+};
+
+export const SubtitleEndAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ subtitleEndAccessory={
+
+ }
+ />
+ ),
+};
+
+export const BottomLabel: Story = {
+ render: (args) => (
+
+ ),
+};
+
+export const BottomLabelEndAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ amount={USDC_AMOUNT}
+ bottomLabel={USDC_PRICE_CHANGE_BOTTOM_LABEL}
+ bottomLabelEndAccessory={}
+ bottomLabelProps={{ color: TextColor.SuccessDefault }}
+ />
+ ),
+};
+
+export const BottomAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ amount={USDC_AMOUNT}
+ bottomAccessory={
+
+
+
+ Stablecoin prices can deviate from $1. Verify the asset and network
+ before you trade.
+
+
+ }
+ />
+ ),
+};
diff --git a/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.test.tsx b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.test.tsx
new file mode 100644
index 000000000..2cba6136a
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.test.tsx
@@ -0,0 +1,541 @@
+// Third party dependencies.
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+import { renderHook } from '@testing-library/react-hooks';
+import { render } from '@testing-library/react-native';
+import React from 'react';
+import { Text } from 'react-native';
+
+// Internal dependencies.
+import { TitleSubpage } from './TitleSubpage';
+
+const CONTAINER_TEST_ID = 'title-subpage-container';
+const AMOUNT_TEST_ID = 'title-subpage-amount';
+const TITLE_ROW_TEST_ID = 'title-subpage-title';
+const SUBTITLE_ROW_TEST_ID = 'title-subpage-subtitle';
+const BOTTOM_LABEL_TEST_ID = 'title-subpage-bottom-label';
+const TITLE_AVATAR_TEST_ID = 'title-subpage-title-avatar';
+const TITLE_AVATAR_SLOT_TEST_ID = 'title-subpage-title-avatar-slot';
+const IDENTITY_ROW_TEST_ID = 'title-subpage-identity-row';
+
+const defaultTitleAvatar = ;
+
+describe('TitleSubpage', () => {
+ let tw: ReturnType;
+
+ beforeAll(() => {
+ tw = renderHook(() => useTailwind()).result.current;
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('rendering', () => {
+ it('renders string title', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Section')).toBeOnTheScreen();
+ });
+
+ it('renders titleAvatar in the identity row', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(TITLE_AVATAR_TEST_ID)).toBeOnTheScreen();
+ });
+
+ it('passes titleAvatar through as startAccessory without a fixed-size overflow wrapper', () => {
+ const { getByTestId, queryByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(TITLE_AVATAR_TEST_ID)).toBeOnTheScreen();
+ expect(queryByTestId(TITLE_AVATAR_SLOT_TEST_ID)).not.toBeOnTheScreen();
+ });
+
+ it('forwards identityRowProps testID to identity BoxRow', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(IDENTITY_ROW_TEST_ID)).toBeOnTheScreen();
+ });
+
+ it('renders string amount when provided', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ });
+
+ it('renders React node amount', () => {
+ const { getByTestId } = render(
+ Custom amount}
+ />,
+ );
+
+ expect(getByTestId('title-subpage-amount-node')).toBeOnTheScreen();
+ });
+
+ it('renders container with testID when provided', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(CONTAINER_TEST_ID)).toBeOnTheScreen();
+ });
+
+ it('forwards amountProps testID to amount Text when amount is a string', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(AMOUNT_TEST_ID)).toBeOnTheScreen();
+ });
+ });
+
+ describe('when title is provided', () => {
+ it('renders title and amount', () => {
+ const { getByText } = render(
+ Custom Top}
+ amount="$4.42"
+ />,
+ );
+
+ expect(getByText('Custom Top')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ });
+
+ it('renders title and titleEndAccessory', () => {
+ const { getByText } = render(
+ Title extra}
+ />,
+ );
+
+ expect(getByText('Step 1')).toBeOnTheScreen();
+ expect(getByText('Title extra')).toBeOnTheScreen();
+ });
+
+ it('forwards titleProps testID to title row Text when title is a string', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(TITLE_ROW_TEST_ID)).toBeOnTheScreen();
+ });
+ });
+
+ describe('when title is false', () => {
+ it('does not render title node', () => {
+ const showTitle = false;
+ const { getByText, queryByTestId } = render(
+ Top
+ ) : (
+ false
+ )
+ }
+ amount="$4.42"
+ />,
+ );
+
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(queryByTestId('title-subpage-title-slot')).not.toBeOnTheScreen();
+ });
+
+ it('does not render title row or titleEndAccessory', () => {
+ const showTitle = false;
+ const { getByText, queryByTestId, queryByText } = render(
+ Top
+ ) : (
+ false
+ )
+ }
+ amount="$4.42"
+ titleEndAccessory={Only title accessory}
+ />,
+ );
+
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(queryByText('Only title accessory')).not.toBeOnTheScreen();
+ expect(queryByTestId('title-subpage-title-slot')).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when titleEndAccessory is false', () => {
+ it('does not render titleEndAccessory', () => {
+ const showTitleEndAccessory = false;
+ const { getByText, queryByTestId } = render(
+ Accessory
+ ) : (
+ false
+ )
+ }
+ />,
+ );
+
+ expect(getByText('Hi')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(
+ queryByTestId('title-subpage-title-end-accessory'),
+ ).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when subtitle is provided', () => {
+ it('renders string subtitle between title and amount', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(getByText('Account 1')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ });
+
+ it('renders subtitle and subtitleEndAccessory', () => {
+ const { getByText } = render(
+ Sub extra}
+ />,
+ );
+
+ expect(getByText('Extra context')).toBeOnTheScreen();
+ expect(getByText('Sub extra')).toBeOnTheScreen();
+ });
+
+ it('forwards subtitleProps testID to subtitle row Text when subtitle is a string', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(SUBTITLE_ROW_TEST_ID)).toBeOnTheScreen();
+ });
+ });
+
+ describe('when subtitle is false', () => {
+ it('does not render subtitle row or subtitleEndAccessory', () => {
+ const showSubtitle = false;
+ const { getByText, queryByTestId, queryByText } = render(
+ Sub
+ ) : (
+ false
+ )
+ }
+ amount="$4.42"
+ subtitleEndAccessory={Only sub accessory}
+ />,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(queryByText('Only sub accessory')).not.toBeOnTheScreen();
+ expect(
+ queryByTestId('title-subpage-subtitle-slot'),
+ ).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when amount is false', () => {
+ it('does not render amount node', () => {
+ const showAmount = false;
+ const { getByText, queryByTestId } = render(
+ $1
+ ) : (
+ false
+ )
+ }
+ />,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(queryByTestId('title-subpage-amount-slot')).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when bottomLabel is provided', () => {
+ it('renders bottomLabel text', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('0.002 ETH')).toBeOnTheScreen();
+ });
+
+ it('renders bottomLabel and bottomLabelEndAccessory', () => {
+ const { getByText } = render(
+ Fee info}
+ />,
+ );
+
+ expect(getByText('0.002 ETH')).toBeOnTheScreen();
+ expect(getByText('Fee info')).toBeOnTheScreen();
+ });
+
+ it('forwards bottomLabelProps testID to bottom label Text', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(BOTTOM_LABEL_TEST_ID)).toBeOnTheScreen();
+ });
+ });
+
+ describe('when bottomAccessory is provided', () => {
+ it('renders bottomAccessory when bottomLabel is omitted', () => {
+ const { getByText } = render(
+ Custom Bottom}
+ />,
+ );
+
+ expect(getByText('Custom Bottom')).toBeOnTheScreen();
+ });
+ });
+
+ describe('when bottomLabel and bottomAccessory are both provided', () => {
+ it('renders only bottomLabel', () => {
+ const { getByText, queryByText } = render(
+ Accessory}
+ />,
+ );
+
+ expect(getByText('Label Priority')).toBeOnTheScreen();
+ expect(queryByText('Accessory')).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when bottomLabel is omitted and bottomLabelEndAccessory is provided', () => {
+ it('does not render bottomLabelEndAccessory without bottomLabel', () => {
+ const { queryByText } = render(
+ Only accessory}
+ bottomAccessory={Full row}
+ />,
+ );
+
+ expect(queryByText('Only accessory')).not.toBeOnTheScreen();
+ });
+
+ it('renders bottomAccessory', () => {
+ const { getByText } = render(
+ Only accessory}
+ bottomAccessory={Full row}
+ />,
+ );
+
+ expect(getByText('Full row')).toBeOnTheScreen();
+ });
+ });
+
+ describe('when amountEndAccessory is provided', () => {
+ it('renders amount and amountEndAccessory', () => {
+ const { getByText } = render(
+ Info}
+ />,
+ );
+
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(getByText('Info')).toBeOnTheScreen();
+ });
+
+ it('does not render amount row when amount is an empty string', () => {
+ const { getByText, queryByText } = render(
+ Accessory only}
+ />,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(queryByText('Accessory only')).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when amountEndAccessory is false', () => {
+ it('does not render amountEndAccessory', () => {
+ const showAmountEndAccessory = false;
+ const { getByText, queryByTestId } = render(
+ Accessory
+ ) : (
+ false
+ )
+ }
+ />,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(
+ queryByTestId('title-subpage-amount-end-accessory'),
+ ).not.toBeOnTheScreen();
+ });
+ });
+
+ describe('when title, amountEndAccessory, and bottomLabel are provided', () => {
+ it('renders all slots', () => {
+ const { getByText } = render(
+ Send}
+ amount="$4.42"
+ amountEndAccessory={i}
+ bottomLabel="0.002 ETH"
+ />,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(getByText('i')).toBeOnTheScreen();
+ expect(getByText('0.002 ETH')).toBeOnTheScreen();
+ });
+ });
+
+ describe('style and twClassName', () => {
+ it('applies custom style to root container', () => {
+ const customStyle = { opacity: 0.5 };
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId(CONTAINER_TEST_ID)).toHaveStyle(customStyle);
+ });
+
+ it('merges twClassName with base styles', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ const container = getByTestId(CONTAINER_TEST_ID);
+
+ expect(container).toHaveStyle(tw`bg-default`);
+ });
+ });
+});
diff --git a/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.tsx b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.tsx
new file mode 100644
index 000000000..ed5a73a2e
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.tsx
@@ -0,0 +1,134 @@
+// Third party dependencies.
+import {
+ FontWeight,
+ TextColor,
+ TextVariant,
+} from '@metamask/design-system-shared';
+import React from 'react';
+
+// Internal dependencies.
+import { Box } from '../Box';
+import { BoxRow } from '../BoxRow';
+
+import type { TitleSubpageProps } from './TitleSubpage.types';
+
+/**
+ * Displays a required identity row (avatar + title stack) with optional subtitle, amount, inline accessories, and bottom rows in a left-aligned layout.
+ * Remaining `View` props are forwarded to the root `Box`.
+ *
+ * @param props - Component props
+ * @param props.title - Title row content (required)
+ * @param props.titleAvatar - Leading visual for the identity row (required); passed through as the identity `BoxRow` `startAccessory` so callers control layout and composition (for example badges that extend past the token)
+ * @param props.identityRowProps - Optional props spread onto the identity `BoxRow` after defaults (`children`, `startAccessory`, and `textProps` are reserved)
+ * @param props.titleColumnProps - Optional props spread onto the title/subtitle column `Box` (`children` is reserved)
+ * @param props.bottomLabelWrapperProps - Optional props spread onto the bottom label `BoxRow` after defaults (`children`, `endAccessory`, and `textProps` are reserved)
+ * @param props.titleEndAccessory - Optional inline accessory to the right of `title`
+ * @param props.subtitle - Optional subtitle row below the title and above the amount
+ * @param props.subtitleEndAccessory - Optional inline accessory to the right of `subtitle`
+ * @param props.amount - Optional primary amount below the title
+ * @param props.amountEndAccessory - Optional inline accessory to the right of the amount
+ * @param props.bottomAccessory - Optional custom bottom row when the bottom label row is not shown
+ * @param props.bottomLabel - Optional secondary label below the amount row
+ * @param props.bottomLabelEndAccessory - Optional inline accessory to the right of `bottomLabel`
+ * @param props.titleProps - Optional props merged into title row `Text` when `title` is a string
+ * @param props.subtitleProps - Optional props merged into subtitle row `Text` when `subtitle` is a string
+ * @param props.amountProps - Optional props merged into amount `Text` when `amount` is a string
+ * @param props.bottomLabelProps - Optional props merged into bottom label `Text` when `bottomLabel` is a string
+ * @param props.twClassName - Optional Tailwind classes on the root container
+ *
+ * @returns The rendered TitleSubpage layout.
+ */
+export const TitleSubpage: React.FC = ({
+ amount,
+ amountEndAccessory,
+ title,
+ titleAvatar,
+ titleEndAccessory,
+ subtitle,
+ subtitleEndAccessory,
+ bottomAccessory,
+ bottomLabel,
+ bottomLabelEndAccessory,
+ amountProps,
+ titleProps,
+ subtitleProps,
+ bottomLabelProps,
+ identityRowProps,
+ titleColumnProps,
+ bottomLabelWrapperProps,
+ twClassName = '',
+ ...props
+}) => {
+ return (
+
+ {/* Identity Row */}
+
+ {/* Title and Subtitle Column */}
+
+ {/* Title Row */}
+ {title && (
+
+ {title}
+
+ )}
+ {/* Subtitle Row */}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ {/* Amount Row */}
+ {amount && (
+
+ {amount}
+
+ )}
+ {/* Bottom Label Row */}
+ {bottomLabel && (
+
+ {bottomLabel}
+
+ )}
+ {!bottomLabel && bottomAccessory}
+
+ );
+};
+
+TitleSubpage.displayName = 'TitleSubpage';
diff --git a/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.types.ts b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.types.ts
new file mode 100644
index 000000000..73f264168
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.types.ts
@@ -0,0 +1,53 @@
+// Third party dependencies.
+import type { TitleSubpagePropsShared } from '@metamask/design-system-shared';
+import type { ViewProps } from 'react-native';
+
+// Internal dependencies.
+import type { BoxProps } from '../Box/Box.types';
+import type { BoxRowProps } from '../BoxRow/BoxRow.types';
+import type { TextProps } from '../Text/Text.types';
+
+/**
+ * TitleSubpage component props (React Native).
+ * Extends {@link TitleSubpagePropsShared} (requires `title` and `titleAvatar`) with platform `Text` passthroughs, `twClassName`, and `View` props.
+ */
+export type TitleSubpageProps = TitleSubpagePropsShared & {
+ /**
+ * Optional props spread onto the identity {@link BoxRow} (excluding `children`, `startAccessory`, and `textProps`, which the component owns).
+ */
+ identityRowProps?: Omit<
+ Partial,
+ 'children' | 'startAccessory' | 'textProps'
+ >;
+ /**
+ * Optional props spread onto the title/subtitle column {@link Box} (excluding `children`, which the component owns).
+ */
+ titleColumnProps?: Omit, 'children'>;
+ /**
+ * Optional props spread onto the bottom label {@link BoxRow} (excluding `children`, `endAccessory`, and `textProps`, which the component owns).
+ */
+ bottomLabelWrapperProps?: Omit<
+ Partial,
+ 'children' | 'endAccessory' | 'textProps'
+ >;
+ /**
+ * Optional props merged into {@link BoxRow} `textProps` when `amount` is a string.
+ */
+ amountProps?: Omit, 'children'>;
+ /**
+ * Optional props merged into {@link BoxRow} `textProps` when `title` is a string.
+ */
+ titleProps?: Omit, 'children'>;
+ /**
+ * Optional props merged into {@link BoxRow} `textProps` when `subtitle` is a string.
+ */
+ subtitleProps?: Omit, 'children'>;
+ /**
+ * Optional props merged into {@link BoxRow} `textProps` when `bottomLabel` is a string.
+ */
+ bottomLabelProps?: Omit, 'children'>;
+ /**
+ * Optional Tailwind class name to apply to the container.
+ */
+ twClassName?: string;
+} & Omit;
diff --git a/packages/design-system-react-native/src/components/TitleSubpage/index.ts b/packages/design-system-react-native/src/components/TitleSubpage/index.ts
new file mode 100644
index 000000000..584779c9e
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/index.ts
@@ -0,0 +1,2 @@
+export { TitleSubpage } from './TitleSubpage';
+export type { TitleSubpageProps } from './TitleSubpage.types';
diff --git a/packages/design-system-react-native/src/components/index.ts b/packages/design-system-react-native/src/components/index.ts
index ad2a17ed1..68f69acca 100644
--- a/packages/design-system-react-native/src/components/index.ts
+++ b/packages/design-system-react-native/src/components/index.ts
@@ -210,6 +210,9 @@ export type { TitleHubProps, TitleHubPropsShared } from './TitleHub';
export { TitleStandard } from './TitleStandard';
export type { TitleStandardProps } from './TitleStandard';
+export { TitleSubpage } from './TitleSubpage';
+export type { TitleSubpageProps } from './TitleSubpage';
+
export {
Toast,
ToastVariant,
diff --git a/packages/design-system-shared/src/index.ts b/packages/design-system-shared/src/index.ts
index ac9df24b2..ff5627b30 100644
--- a/packages/design-system-shared/src/index.ts
+++ b/packages/design-system-shared/src/index.ts
@@ -41,6 +41,9 @@ export { type TitleHubPropsShared } from './types/TitleHub';
// TitleStandard types (ADR-0004)
export { type TitleStandardPropsShared } from './types/TitleStandard';
+// TitleSubpage types (ADR-0004)
+export { type TitleSubpagePropsShared } from './types/TitleSubpage';
+
// BoxColumn types (ADR-0004)
export { type BoxColumnPropsShared } from './types/BoxColumn';
diff --git a/packages/design-system-shared/src/types/TitleSubpage/TitleSubpage.types.ts b/packages/design-system-shared/src/types/TitleSubpage/TitleSubpage.types.ts
new file mode 100644
index 000000000..0215daf2b
--- /dev/null
+++ b/packages/design-system-shared/src/types/TitleSubpage/TitleSubpage.types.ts
@@ -0,0 +1,55 @@
+import type { ReactNode } from 'react';
+
+/**
+ * TitleSubpage component shared props (ADR-0004).
+ * Platform-independent properties; platform packages extend with `ViewProps` / `className`,
+ * `twClassName`, and platform `Text` prop passthroughs.
+ */
+export type TitleSubpagePropsShared = {
+ /**
+ * Optional primary amount line below the title and optional subtitle (for example a fiat or token value).
+ * When a string, platforms typically wrap with large display styles via `textProps`.
+ * The amount row renders when `amount` is truthy; `amountEndAccessory` only appears on that row and does not show the row without `amount`.
+ */
+ amount?: ReactNode;
+ /**
+ * Optional accessory rendered inline to the right of the amount.
+ */
+ amountEndAccessory?: ReactNode;
+ /**
+ * Title row above the optional amount (via platform `textProps` when a string). Required.
+ */
+ title: ReactNode;
+ /**
+ * Leading visual for the identity row (for example an avatar). On React Native this is passed as
+ * the `startAccessory` of the identity `BoxRow` (typically size the avatar to the design-spec 40×40 slot).
+ */
+ titleAvatar: ReactNode;
+ /**
+ * Optional accessory rendered inline to the right of `title` in the title row.
+ */
+ titleEndAccessory?: ReactNode;
+ /**
+ * Optional subtitle row below the title and above the amount (via platform `textProps` when a string).
+ * The subtitle row renders when `subtitle` is renderable.
+ */
+ subtitle?: ReactNode;
+ /**
+ * Optional accessory rendered inline to the right of `subtitle` in the subtitle row.
+ */
+ subtitleEndAccessory?: ReactNode;
+ /**
+ * Optional custom bottom row when `bottomLabel` is not truthy.
+ * Mutually exclusive with the bottom label row: only one bottom row is shown.
+ */
+ bottomAccessory?: ReactNode;
+ /**
+ * Optional bottom row with secondary label styling when a string (via platform `textProps`).
+ * When `bottomLabel` is truthy, that row is shown instead of `bottomAccessory`; `bottomLabelEndAccessory` only appears with a truthy `bottomLabel`.
+ */
+ bottomLabel?: ReactNode;
+ /**
+ * Optional accessory rendered inline to the right of `bottomLabel` in the bottom label row.
+ */
+ bottomLabelEndAccessory?: ReactNode;
+};
diff --git a/packages/design-system-shared/src/types/TitleSubpage/index.ts b/packages/design-system-shared/src/types/TitleSubpage/index.ts
new file mode 100644
index 000000000..e8cd6b321
--- /dev/null
+++ b/packages/design-system-shared/src/types/TitleSubpage/index.ts
@@ -0,0 +1 @@
+export type { TitleSubpagePropsShared } from './TitleSubpage.types';