diff --git a/apps/storybook-react-native/.storybook/storybook.requires.js b/apps/storybook-react-native/.storybook/storybook.requires.js
index 599589f3e..405be4a2d 100644
--- a/apps/storybook-react-native/.storybook/storybook.requires.js
+++ b/apps/storybook-react-native/.storybook/storybook.requires.js
@@ -115,6 +115,7 @@ const getStories = () => {
"./../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextButton/TextButton.stories.tsx"),
"./../../packages/design-system-react-native/src/components/TextField/TextField.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextField/TextField.stories.tsx"),
"./../../packages/design-system-react-native/src/components/TextFieldSearch/TextFieldSearch.stories.tsx": require("../../../packages/design-system-react-native/src/components/TextFieldSearch/TextFieldSearch.stories.tsx"),
+ "./../../packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.stories.tsx": require("../../../packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.stories.tsx"),
"./../../packages/design-system-react-native/src/components/Toast/Toast.stories.tsx": require("../../../packages/design-system-react-native/src/components/Toast/Toast.stories.tsx"),
"./stories/Backgrounds.stories.tsx": require("../stories/Backgrounds.stories.tsx"),
"./stories/WalletHome.stories.tsx": require("../stories/WalletHome.stories.tsx"),
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..2c52f37a7
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/README.md
@@ -0,0 +1,444 @@
+# 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 rendered in a 40×40 centered slot as the `startAccessory` of the identity `BoxRow`.
+
+```tsx
+import { TitleSubpage, Box } from '@metamask/design-system-react-native';
+
+
+ }
+ title="Send"
+/>;
+```
+
+## Props
+
+### `titleAvatar`
+
+Leading visual for the identity row (required). Rendered in a 40×40 box with content centered horizontally and vertically, as the `startAccessory` of the identity `BoxRow`. Typically pass an avatar or token component (for example `AvatarToken`).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ------- |
+| `ReactNode` | Yes | — |
+
+```tsx
+import { TitleSubpage, Box } from '@metamask/design-system-react-native';
+
+
+ }
+ title="Send"
+ amount="$4.42"
+/>;
+```
+
+### `title`
+
+Title row (required). When `title` is a string, it uses `TextVariant.HeadingSm` and `TextColor.TextDefault` (merged with `titleProps`). Pass a `ReactNode` for custom layout. The row also renders when only `titleEndAccessory` is renderable (for example `title={false}` with an end accessory).
+
+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 { TitleSubpage, Box } 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`).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ TitleSubpage,
+ Box,
+ Icon,
+ IconName,
+ IconSize,
+} from '@metamask/design-system-react-native';
+
+
+ }
+ title="Send"
+ titleEndAccessory={
+
+
+
+ }
+ amount="$4.42"
+/>;
+```
+
+### `subtitle`
+
+Optional subtitle row between the title and the amount. When `subtitle` is a string, it uses `TextVariant.BodySm`, medium weight, and `TextColor.TextAlternative` (merged with `subtitleProps`). Pass a `ReactNode` for custom layout. The row also renders when only `subtitleEndAccessory` is renderable (for example `subtitle={false}` with an end accessory).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import { TitleSubpage, Box } 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`).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ TitleSubpage,
+ Box,
+ Icon,
+ IconName,
+ IconSize,
+} 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` or `amountEndAccessory` is renderable. 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 { TitleSubpage, Box } 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).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ TitleSubpage,
+ Box,
+ Icon,
+ IconName,
+ IconSize,
+} from '@metamask/design-system-react-native';
+
+
+ }
+ title="Send"
+ amount="$4.42"
+ amountEndAccessory={
+
+
+
+ }
+/>;
+```
+
+### `bottomLabel`
+
+Optional bottom label row. When `bottomLabel` is a string, it uses `TextVariant.BodySm`, medium weight, and `TextColor.TextAlternative` (merged with `bottomLabelProps`). If `bottomLabel` or `bottomLabelEndAccessory` is renderable, that row is shown instead of `bottomAccessory`.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import { TitleSubpage, Box } 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).
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ TitleSubpage,
+ Box,
+ Text,
+ TextColor,
+ FontWeight,
+ TextVariant,
+} 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 neither `bottomLabel` nor `bottomLabelEndAccessory` is renderable. Renders without default label typography; compose layout inside the node.
+
+| TYPE | REQUIRED | DEFAULT |
+| ----------- | -------- | ----------- |
+| `ReactNode` | No | `undefined` |
+
+```tsx
+import {
+ TitleSubpage,
+ Box,
+ BoxFlexDirection,
+ BoxAlignItems,
+ Icon,
+ IconName,
+ IconSize,
+ Text,
+ TextVariant,
+} 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 |
+| -------------------- | -------- | ----------- |
+| `Partial` | No | `undefined` |
+
+```tsx
+import { TitleSubpage, Box } 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 |
+| -------------------- | -------- | ----------- |
+| `Partial` | No | `undefined` |
+
+```tsx
+import { TitleSubpage, Box } 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 |
+| -------------------- | -------- | ----------- |
+| `Partial` | No | `undefined` |
+
+```tsx
+import { TitleSubpage, Box } 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 |
+| -------------------- | -------- | ----------- |
+| `Partial` | No | `undefined` |
+
+```tsx
+import { TitleSubpage, Box } from '@metamask/design-system-react-native';
+
+
+ }
+ title="Send"
+ amount="$4.42"
+ bottomLabel="0.002 ETH"
+ bottomLabelProps={{ testID: 'title-subpage-bottom' }}
+/>;
+```
+
+### `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 { TitleSubpage, Box } 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 { TitleSubpage, Box } 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..77be92eff
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.stories.tsx
@@ -0,0 +1,235 @@
+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, TextColor, FontWeight, TextVariant } 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 AmountAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ amount={USDC_AMOUNT}
+ amountEndAccessory={
+
+
+
+ }
+ />
+ ),
+};
+
+export const Title: Story = {
+ render: () => (
+ } title={USDC_TITLE} />
+ ),
+};
+
+export const TitleAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ titleEndAccessory={}
+ />
+ ),
+};
+
+export const Subtitle: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ />
+ ),
+};
+
+export const SubtitleAccessory: Story = {
+ render: () => (
+ }
+ title={USDC_TITLE}
+ subtitle={USDC_SUBTITLE}
+ subtitleEndAccessory={
+
+
+
+ }
+ />
+ ),
+};
+
+export const BottomLabel: Story = {
+ render: (args) => (
+
+ ),
+};
+
+export const BottomLabelAccessory: 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..1170a9d21
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.test.tsx
@@ -0,0 +1,461 @@
+// 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 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('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();
+ });
+ });
+
+ describe('when titleEndAccessory is false', () => {
+ it('renders title only', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Hi')).toBeOnTheScreen();
+ expect(getByText('$4.42')).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 node but shows accessory when subtitleEndAccessory is provided', () => {
+ const showSubtitle = false;
+ const { getByText, queryByTestId } = render(
+ Sub
+ ) : (
+ false
+ )
+ }
+ amount="$4.42"
+ subtitleEndAccessory={Only sub accessory}
+ />,
+ );
+
+ expect(getByText('Send')).toBeOnTheScreen();
+ expect(getByText('$4.42')).toBeOnTheScreen();
+ expect(getByText('Only sub accessory')).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 only bottomLabelEndAccessory is provided', () => {
+ it('renders bottom label row with accessory and not bottomAccessory', () => {
+ const { getByText, queryByText } = render(
+ Only accessory}
+ bottomAccessory={Full row}
+ />,
+ );
+
+ expect(getByText('Only accessory')).toBeOnTheScreen();
+ expect(queryByText('Full row')).not.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('renders amountEndAccessory when amount is an empty string', () => {
+ const { getByText } = render(
+ Accessory only}
+ />,
+ );
+
+ expect(getByText('Accessory only')).toBeOnTheScreen();
+ });
+ });
+
+ describe('when amountEndAccessory is false', () => {
+ it('renders amount only', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('$4.42')).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..5ad21676c
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.tsx
@@ -0,0 +1,171 @@
+// Third party dependencies.
+import { isReactNodeRenderable } from '@metamask/design-system-shared';
+import React from 'react';
+
+// Internal dependencies.
+import { BoxAlignItems, BoxJustifyContent } from '../../types';
+import { Box } from '../Box';
+import { BoxRow } from '../BoxRow';
+import { TextVariant, TextColor, FontWeight } from '../Text';
+
+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); rendered in a 40×40 centered slot
+ * @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,
+ twClassName = '',
+ ...props
+}) => {
+ const amountEndAccessoryNode = isReactNodeRenderable(amountEndAccessory)
+ ? amountEndAccessory
+ : undefined;
+
+ const titleEndAccessoryNode = isReactNodeRenderable(titleEndAccessory)
+ ? titleEndAccessory
+ : undefined;
+
+ const subtitleEndAccessoryNode = isReactNodeRenderable(subtitleEndAccessory)
+ ? subtitleEndAccessory
+ : undefined;
+
+ const bottomLabelEndAccessoryNode = isReactNodeRenderable(
+ bottomLabelEndAccessory,
+ )
+ ? bottomLabelEndAccessory
+ : undefined;
+
+ const renderTitleRow =
+ isReactNodeRenderable(title) || isReactNodeRenderable(titleEndAccessory);
+ const renderSubtitleRow =
+ isReactNodeRenderable(subtitle) ||
+ isReactNodeRenderable(subtitleEndAccessory);
+ const renderAmountRow =
+ isReactNodeRenderable(amount) || isReactNodeRenderable(amountEndAccessory);
+ const renderBottomLabelRow =
+ isReactNodeRenderable(bottomLabel) ||
+ isReactNodeRenderable(bottomLabelEndAccessory);
+ const renderBottomAccessory =
+ !renderBottomLabelRow && isReactNodeRenderable(bottomAccessory);
+
+ const titleRow = (
+
+ {title}
+
+ );
+
+ const subtitleRow = (
+
+ {subtitle}
+
+ );
+
+ const titleAvatarSlot = (
+
+ {titleAvatar}
+
+ );
+
+ const identityRow = (
+
+
+ {renderTitleRow ? titleRow : null}
+ {renderSubtitleRow ? subtitleRow : null}
+
+
+ );
+
+ const amountRow = (
+
+ {amount}
+
+ );
+
+ const bottomLabelRow = (
+
+ {bottomLabel}
+
+ );
+
+ return (
+
+ {identityRow}
+ {renderAmountRow ? amountRow : null}
+ {renderBottomLabelRow ? bottomLabelRow : null}
+ {renderBottomAccessory ? bottomAccessory : null}
+
+ );
+};
+
+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..add6a887e
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/TitleSubpage.types.ts
@@ -0,0 +1,33 @@
+// Third party dependencies.
+import type { TitleSubpagePropsShared } from '@metamask/design-system-shared';
+import type { ViewProps } from 'react-native';
+
+// Internal dependencies.
+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 merged into {@link BoxHorizontal} `textProps` when `amount` is a string.
+ */
+ amountProps?: Partial;
+ /**
+ * Optional props merged into {@link BoxHorizontal} `textProps` when `title` is a string.
+ */
+ titleProps?: Partial;
+ /**
+ * Optional props merged into {@link BoxHorizontal} `textProps` when `subtitle` is a string.
+ */
+ subtitleProps?: Partial;
+ /**
+ * Optional props merged into {@link BoxHorizontal} `textProps` when `bottomLabel` is a string.
+ */
+ bottomLabelProps?: Partial;
+ /**
+ * 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..d4da6b2dd
--- /dev/null
+++ b/packages/design-system-react-native/src/components/TitleSubpage/index.ts
@@ -0,0 +1,3 @@
+export type { TitleSubpagePropsShared } from '@metamask/design-system-shared';
+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 e057985d2..bce3504cf 100644
--- a/packages/design-system-react-native/src/components/index.ts
+++ b/packages/design-system-react-native/src/components/index.ts
@@ -204,6 +204,12 @@ export type { TextFieldSearchProps } from './TextFieldSearch';
export { TextOrChildren } from './temp-components/TextOrChildren';
export type { TextOrChildrenProps } from './temp-components/TextOrChildren';
+export { TitleSubpage } from './TitleSubpage';
+export type {
+ TitleSubpageProps,
+ TitleSubpagePropsShared,
+} from './TitleSubpage';
+
export {
Toast,
ToastVariant,
diff --git a/packages/design-system-shared/src/index.ts b/packages/design-system-shared/src/index.ts
index c16cb2e07..daa746976 100644
--- a/packages/design-system-shared/src/index.ts
+++ b/packages/design-system-shared/src/index.ts
@@ -37,12 +37,15 @@ export { type BannerBasePropsShared } from './types/BannerBase';
// TextOrChildren types (ADR-0004)
export { type TextOrChildrenPropsShared } from './types/TextOrChildren';
-// BoxRow types (ADR-0004)
-export { type BoxRowPropsShared } from './types/BoxRow';
+// TitleSubpage types (ADR-0004)
+export { type TitleSubpagePropsShared } from './types/TitleSubpage';
// BoxColumn types (ADR-0004)
export { type BoxColumnPropsShared } from './types/BoxColumn';
+// BoxRow types (ADR-0004)
+export { type BoxRowPropsShared } from './types/BoxRow';
+
// HeaderSearch types (ADR-0003 + ADR-0004)
export {
HeaderSearchVariant,
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..35fe3174d
--- /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` or `amountEndAccessory` is renderable.
+ */
+ 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 rendered
+ * in a 40×40 box, centered, as the `startAccessory` of the identity `BoxRow`.
+ */
+ 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` or `subtitleEndAccessory` is renderable.
+ */
+ subtitle?: ReactNode;
+ /**
+ * Optional accessory rendered inline to the right of `subtitle` in the subtitle row.
+ */
+ subtitleEndAccessory?: ReactNode;
+ /**
+ * Optional custom bottom row when neither `bottomLabel` nor `bottomLabelEndAccessory` is renderable.
+ * 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`).
+ * If `bottomLabel` or `bottomLabelEndAccessory` is renderable, that row is shown instead of `bottomAccessory`.
+ */
+ 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';