diff --git a/.nx/version-plans/version-plan-1773849698407.md b/.nx/version-plans/version-plan-1773849698407.md
new file mode 100644
index 000000000..d3652d2ee
--- /dev/null
+++ b/.nx/version-plans/version-plan-1773849698407.md
@@ -0,0 +1,5 @@
+---
+'@ledgerhq/lumen-ui-rnative': patch
+---
+
+feat(MediaCard): introduce MediaCard component with docs, tests, and figma code connect.
diff --git a/.nx/version-plans/version-plan-1774000912718.md b/.nx/version-plans/version-plan-1774000912718.md
new file mode 100644
index 000000000..023408aea
--- /dev/null
+++ b/.nx/version-plans/version-plan-1774000912718.md
@@ -0,0 +1,5 @@
+---
+'@ledgerhq/lumen-ui-rnative': patch
+---
+
+- fix(ui-rnative): increase the width spacing of AmountDisplay char to prevent overflow
diff --git a/.nx/version-plans/version-plan-1774000912719.md b/.nx/version-plans/version-plan-1774000912719.md
new file mode 100644
index 000000000..44f58ecc1
--- /dev/null
+++ b/.nx/version-plans/version-plan-1774000912719.md
@@ -0,0 +1,6 @@
+---
+'@ledgerhq/lumen-ui-react': patch
+---
+
+- fix(ui-react): prevent baseline issues on AmountDisplay component
+- fix(ui-react): increase the width spacing of AmountDisplay char to prevent overflow
diff --git a/apps/app-sandbox-rnative/src/app/App.tsx b/apps/app-sandbox-rnative/src/app/App.tsx
index 5b2eefc83..57194feb6 100644
--- a/apps/app-sandbox-rnative/src/app/App.tsx
+++ b/apps/app-sandbox-rnative/src/app/App.tsx
@@ -46,6 +46,7 @@ import {
Banners,
CardButtons,
ContentBanners,
+ MediaCards,
Tooltips,
ListItems,
Gradients,
@@ -181,6 +182,9 @@ const AppContent = ({
+
+
+
diff --git a/apps/app-sandbox-rnative/src/app/blocks/MediaCards.tsx b/apps/app-sandbox-rnative/src/app/blocks/MediaCards.tsx
new file mode 100644
index 000000000..efc555e0a
--- /dev/null
+++ b/apps/app-sandbox-rnative/src/app/blocks/MediaCards.tsx
@@ -0,0 +1,35 @@
+import {
+ Box,
+ MediaCard,
+ MediaCardTitle,
+ Tag,
+} from '@ledgerhq/lumen-ui-rnative';
+
+const EXAMPLE_SRC =
+ 'https://ledger-wp-website-s3-prd.ledger.com/uploads/2026/03/hero_visual-1.webp';
+
+export const MediaCards = () => {
+ return (
+
+ ({})}
+ onClose={() => ({})}
+ >
+
+
+ Black Friday sale. 3 days with no fees on your transactions.
+
+
+
+ ({})}
+ onClose={() => ({})}
+ >
+ Secure your crypto assets
+ Get started with Ledger and protect your digital assets today.
+
+
+ );
+};
diff --git a/apps/app-sandbox-rnative/src/app/blocks/index.ts b/apps/app-sandbox-rnative/src/app/blocks/index.ts
index f26803349..0a99907c6 100644
--- a/apps/app-sandbox-rnative/src/app/blocks/index.ts
+++ b/apps/app-sandbox-rnative/src/app/blocks/index.ts
@@ -29,5 +29,6 @@ export * from './InteractiveIcons';
export * from './Banners';
export * from './CardButtons';
export * from './ContentBanners';
+export * from './MediaCards';
export * from './Tooltips';
export * from './Gradients';
diff --git a/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx b/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx
index a90255ec2..472a2d3bb 100644
--- a/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx
+++ b/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx
@@ -190,10 +190,14 @@ describe('AmountDisplay', () => {
);
const root = container.firstChild as HTMLElement;
- const children = Array.from(root.children);
- expect(children).toHaveLength(2);
- expect(children[0]).toHaveAttribute('aria-hidden', 'true');
- expect(children[1]).toHaveAttribute('aria-hidden', 'true');
+ // Traverse all descendants (not just immediate children)
+ const allDescendants = Array.from(
+ root.querySelectorAll('[aria-hidden="true"]'),
+ );
+ expect(allDescendants.length).toBe(2);
+ allDescendants.forEach((node) => {
+ expect(node).toHaveAttribute('aria-hidden', 'true');
+ });
});
it('renders group separators', () => {
diff --git a/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.tsx b/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.tsx
index da300583d..1ddb76c48 100644
--- a/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.tsx
+++ b/libs/ui-react/src/lib/Components/AmountDisplay/AmountDisplay.tsx
@@ -9,29 +9,29 @@ import {
} from './types';
const INTEGER_DIGIT_WIDTHS = {
- 0: 24.5,
- 1: 15,
- 2: 23,
- 3: 24,
- 4: 25,
- 5: 23,
- 6: 24.5,
- 7: 21.5,
- 8: 24,
- 9: 24,
+ 0: 25,
+ 1: 15.5,
+ 2: 23.5,
+ 3: 24.5,
+ 4: 25.5,
+ 5: 23.5,
+ 6: 25,
+ 7: 22,
+ 8: 24.5,
+ 9: 24.5,
};
const DECIMAL_DIGIT_WIDTHS = {
- 0: 17,
- 1: 10.5,
- 2: 16,
- 3: 16.5,
- 4: 17.2,
- 5: 15.7,
- 6: 17,
- 7: 14.7,
- 8: 16.5,
- 9: 16.5,
+ 0: 17.5,
+ 1: 11,
+ 2: 16.5,
+ 3: 17,
+ 4: 18,
+ 5: 16,
+ 6: 17.5,
+ 7: 15,
+ 8: 17,
+ 9: 17,
};
const DigitStrip = memo(({ value, animate, type }: DigitStripProps) => {
@@ -136,46 +136,50 @@ export const AmountDisplay = ({
-
- {parts.currencyPosition === 'start' && (
- {parts.currencyText}
- )}
- {hidden ? (
- ••••
- ) : (
-
- )}
-
-
- {!hidden && parts.decimalPart && {parts.decimalSeparator}}
- {parts.decimalPart && !hidden && (
-
- )}
- {parts.currencyPosition === 'end' && (
- {parts.currencyText}
- )}
-
+
+
+ {parts.currencyPosition === 'start' && (
+ {parts.currencyText}
+ )}
+ {hidden ? (
+ ••••
+ ) : (
+
+ )}
+
+
+ {!hidden && parts.decimalPart && (
+ {parts.decimalSeparator}
+ )}
+ {parts.decimalPart && !hidden && (
+
+ )}
+ {parts.currencyPosition === 'end' && (
+ {parts.currencyText}
+ )}
+
+
);
};
diff --git a/libs/ui-rnative/src/i18n/locales/de.json b/libs/ui-rnative/src/i18n/locales/de.json
index 7d5e822fb..9ddf818bd 100644
--- a/libs/ui-rnative/src/i18n/locales/de.json
+++ b/libs/ui-rnative/src/i18n/locales/de.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Schließen"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "QR-Code scannen"
diff --git a/libs/ui-rnative/src/i18n/locales/en.json b/libs/ui-rnative/src/i18n/locales/en.json
index 4fba1aa86..f488514ee 100644
--- a/libs/ui-rnative/src/i18n/locales/en.json
+++ b/libs/ui-rnative/src/i18n/locales/en.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Close"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "Scan QR code"
diff --git a/libs/ui-rnative/src/i18n/locales/es.json b/libs/ui-rnative/src/i18n/locales/es.json
index bfa44557d..cfa38549c 100644
--- a/libs/ui-rnative/src/i18n/locales/es.json
+++ b/libs/ui-rnative/src/i18n/locales/es.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Cerrar"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "Escanear código QR"
diff --git a/libs/ui-rnative/src/i18n/locales/fr.json b/libs/ui-rnative/src/i18n/locales/fr.json
index e01c21f7b..35b22fecd 100644
--- a/libs/ui-rnative/src/i18n/locales/fr.json
+++ b/libs/ui-rnative/src/i18n/locales/fr.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Fermer"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "Scanner le code QR"
diff --git a/libs/ui-rnative/src/i18n/locales/ja.json b/libs/ui-rnative/src/i18n/locales/ja.json
index 05e9d499d..457413add 100644
--- a/libs/ui-rnative/src/i18n/locales/ja.json
+++ b/libs/ui-rnative/src/i18n/locales/ja.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "閉じる"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "QRコードをスキャン"
diff --git a/libs/ui-rnative/src/i18n/locales/ko.json b/libs/ui-rnative/src/i18n/locales/ko.json
index eda729c7d..8e7235ec8 100644
--- a/libs/ui-rnative/src/i18n/locales/ko.json
+++ b/libs/ui-rnative/src/i18n/locales/ko.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "닫기"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "QR 코드 스캔"
diff --git a/libs/ui-rnative/src/i18n/locales/pt.json b/libs/ui-rnative/src/i18n/locales/pt.json
index d217d715d..8e7acb9f3 100644
--- a/libs/ui-rnative/src/i18n/locales/pt.json
+++ b/libs/ui-rnative/src/i18n/locales/pt.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Fechar"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "Escanear QR Code"
diff --git a/libs/ui-rnative/src/i18n/locales/ru.json b/libs/ui-rnative/src/i18n/locales/ru.json
index 3cc7a7fe3..f48742aae 100644
--- a/libs/ui-rnative/src/i18n/locales/ru.json
+++ b/libs/ui-rnative/src/i18n/locales/ru.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Закрыть"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "Сканировать QR-код"
diff --git a/libs/ui-rnative/src/i18n/locales/th.json b/libs/ui-rnative/src/i18n/locales/th.json
index bd77a6503..142b91853 100644
--- a/libs/ui-rnative/src/i18n/locales/th.json
+++ b/libs/ui-rnative/src/i18n/locales/th.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "ปิด"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "สแกน QR Code"
diff --git a/libs/ui-rnative/src/i18n/locales/tr.json b/libs/ui-rnative/src/i18n/locales/tr.json
index 0130f76ca..be0d0bfa2 100644
--- a/libs/ui-rnative/src/i18n/locales/tr.json
+++ b/libs/ui-rnative/src/i18n/locales/tr.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "Kapat"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "Karekodu okut"
diff --git a/libs/ui-rnative/src/i18n/locales/zh.json b/libs/ui-rnative/src/i18n/locales/zh.json
index 74415432b..4509c3fba 100644
--- a/libs/ui-rnative/src/i18n/locales/zh.json
+++ b/libs/ui-rnative/src/i18n/locales/zh.json
@@ -1,4 +1,7 @@
{
+ "common": {
+ "closeAriaLabel": "关闭"
+ },
"components": {
"addressInput": {
"qrCodeAriaLabel": "扫描二维码"
diff --git a/libs/ui-rnative/src/lib/Components/AmountDisplay/AmountDisplay.tsx b/libs/ui-rnative/src/lib/Components/AmountDisplay/AmountDisplay.tsx
index 1c586c629..8ad673f0a 100644
--- a/libs/ui-rnative/src/lib/Components/AmountDisplay/AmountDisplay.tsx
+++ b/libs/ui-rnative/src/lib/Components/AmountDisplay/AmountDisplay.tsx
@@ -21,29 +21,29 @@ import {
} from './types';
const INTEGER_DIGIT_WIDTHS = {
- 0: 24.5,
- 1: 15,
- 2: 23,
- 3: 24,
- 4: 25,
- 5: 23,
- 6: 24.5,
- 7: 21.5,
- 8: 24,
- 9: 24,
+ 0: 25,
+ 1: 15.5,
+ 2: 23.5,
+ 3: 24.5,
+ 4: 25.5,
+ 5: 23.5,
+ 6: 25,
+ 7: 22,
+ 8: 24.5,
+ 9: 24.5,
};
const DECIMAL_DIGIT_WIDTHS = {
- 0: 17,
- 1: 10.5,
- 2: 16,
- 3: 16.5,
- 4: 17.2,
- 5: 15.7,
- 6: 17,
- 7: 14.7,
- 8: 16.5,
- 9: 16.5,
+ 0: 17.5,
+ 1: 11,
+ 2: 16.5,
+ 3: 17,
+ 4: 18,
+ 5: 16,
+ 6: 17.5,
+ 7: 15,
+ 8: 17,
+ 9: 17,
};
const useStyles = () => {
diff --git a/libs/ui-rnative/src/lib/Components/InteractiveIcon/InteractiveIcon.tsx b/libs/ui-rnative/src/lib/Components/InteractiveIcon/InteractiveIcon.tsx
index 60b397359..b6ee8241d 100644
--- a/libs/ui-rnative/src/lib/Components/InteractiveIcon/InteractiveIcon.tsx
+++ b/libs/ui-rnative/src/lib/Components/InteractiveIcon/InteractiveIcon.tsx
@@ -9,13 +9,16 @@ import { Pressable } from '../Utility';
import { HIT_SLOP_MAP, InteractiveIconProps } from './types';
type IconType = InteractiveIconProps['iconType'];
+type Appearance = NonNullable;
const useStyles = ({
iconType,
+ appearance,
pressed,
disabled,
}: {
iconType: IconType;
+ appearance: Appearance;
pressed: boolean;
disabled: boolean;
}) => {
@@ -25,6 +28,21 @@ const useStyles = ({
filled: { backgroundColor: t.colors.bg.base },
stroked: { backgroundColor: t.colors.bg.baseTransparent },
};
+ const appearanceColors = {
+ base: {
+ default: t.colors.text.base,
+ pressed: t.colors.text.basePressed,
+ },
+ muted: {
+ default: t.colors.text.muted,
+ pressed: t.colors.text.mutedPressed,
+ },
+ white: {
+ default: t.colors.text.white,
+ pressed: t.colors.text.whitePressed,
+ },
+ };
+ const colorSet = appearanceColors[appearance];
return {
container: StyleSheet.flatten([
@@ -39,12 +57,12 @@ const useStyles = ({
color: disabled
? t.colors.text.disabled
: pressed
- ? t.colors.text.mutedPressed
- : t.colors.text.muted,
+ ? colorSet.pressed
+ : colorSet.default,
},
};
},
- [iconType, pressed, disabled],
+ [iconType, appearance, pressed, disabled],
);
};
@@ -83,6 +101,7 @@ export const InteractiveIcon = ({
disabled: disabledProp = false,
hitSlop: hitSlopProp,
hitSlopType = 'comfortable',
+ appearance = 'muted',
style,
lx,
...props
@@ -113,6 +132,7 @@ export const InteractiveIcon = ({
{({ pressed }) => (
@@ -125,15 +145,17 @@ export const InteractiveIcon = ({
const InteractiveIconContent = ({
iconType,
+ appearance,
pressed,
disabled,
children,
}: PropsWithChildren<{
iconType: IconType;
+ appearance: Appearance;
pressed: boolean;
disabled: boolean;
}>) => {
- const styles = useStyles({ iconType, pressed, disabled });
+ const styles = useStyles({ iconType, appearance, pressed, disabled });
return (
diff --git a/libs/ui-rnative/src/lib/Components/InteractiveIcon/types.ts b/libs/ui-rnative/src/lib/Components/InteractiveIcon/types.ts
index 4a7374861..1b560c7b1 100644
--- a/libs/ui-rnative/src/lib/Components/InteractiveIcon/types.ts
+++ b/libs/ui-rnative/src/lib/Components/InteractiveIcon/types.ts
@@ -52,6 +52,14 @@ export type InteractiveIconProps = {
* Choose 'filled' for icons with solid backgrounds or 'stroked' for outlined icons.
*/
iconType: 'filled' | 'stroked';
+ /**
+ * The color appearance of the icon.
+ * - `base`: Default high-contrast color.
+ * - `muted`: Subdued color for secondary actions.
+ * - `white`: White color for use on dark backgrounds.
+ * @default 'muted'
+ */
+ appearance?: 'base' | 'muted' | 'white';
/**
* Preset for the touchable area. Ignored if `hitSlop` is passed explicitly.
* Automatically applies insets based on the child's icon size.
diff --git a/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.mdx b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.mdx
new file mode 100644
index 000000000..159b6dd18
--- /dev/null
+++ b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.mdx
@@ -0,0 +1,96 @@
+import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks';
+import * as MediaCardStories from './MediaCard.stories';
+import { CustomTabs, Tab } from '../../../../.storybook/components';
+import CommonRulesDoAndDont from '../../../../.storybook/components/DoVsDont/CommonRulesDoAndDont.mdx';
+
+
+
+# MediaCard
+
+
+
+
+## Introduction
+
+MediaCard is a promotional card component that displays a full-bleed background image with gradient overlays for text readability. It uses a simplified compound pattern with `MediaCardTitle` for the text and free-form children for leading content (e.g. tags, icons).
+
+> View in [Figma](https://www.figma.com/design/JxaLVMTWirCpU0rsbZ30k7?node-id=15160-2853).
+
+## Anatomy
+
+
+
+- **MediaCard**: Root pressable container with background image and gradient overlays
+- **MediaCardTitle**: Styled title text (clamps at 3 lines)
+- **Leading content**: Any element rendered before `MediaCardTitle` (tags, badges, icons) — no wrapper needed
+- **Close button**: Rendered via the `onClose` prop (positioned absolute top-right)
+
+## Properties
+
+
+
+
+### Layout
+
+The card fills its parent width by default.
+
+
+
+### Compositions
+
+Leading content is optional — just place any element before `MediaCardTitle` inside `MediaCard`.
+
+
+
+## Accessibility
+
+- The root element uses `accessibilityRole='button'` and responds to press events
+- The close button includes an `accessibilityLabel` (auto-translated via i18n)
+- The background image is not accessible (`accessible={false}`) since it is decorative
+- Components forward refs and spread props for accessibility support
+
+
+
+
+## Setup
+
+Install and set up the library with our [Setup Guide →](?path=/docs/getting-started-setup--docs).
+
+## Basic Usage
+
+```tsx
+import { MediaCard, MediaCardTitle, Tag } from '@ledgerhq/lumen-ui-rnative';
+
+function MyComponent() {
+ return (
+ console.log('pressed')}
+ onClose={() => console.log('closed')}
+ >
+
+ Card title
+
+ );
+}
+```
+
+### Layout Adjustments with lx
+
+Use the `lx` prop for layout adjustments like margins or positioning:
+
+```tsx
+ console.log('pressed')}
+ onClose={() => console.log('closed')}
+ lx={{ marginTop: 's16', marginBottom: 's8' }}
+>
+ With margin
+
+```
+
+
+
+
+
diff --git a/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.stories.tsx b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.stories.tsx
new file mode 100644
index 000000000..1f6a228c2
--- /dev/null
+++ b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.stories.tsx
@@ -0,0 +1,190 @@
+import { CryptoIcon } from '@ledgerhq/crypto-icons';
+import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
+import { useState } from 'react';
+import { Button } from '../Button';
+import { Tag } from '../Tag';
+import { Box, Text } from '../Utility';
+import { MediaCard, MediaCardTitle } from './MediaCard';
+
+const meta = {
+ component: MediaCard,
+ subcomponents: { MediaCardTitle },
+ title: 'Communication/MediaCard',
+ parameters: {
+ layout: 'centered',
+ backgrounds: { default: 'light' },
+ docs: {
+ source: {
+ language: 'tsx',
+ format: true,
+ type: 'code',
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const EXAMPLE_SRC =
+ 'https://ledger-wp-website-s3-prd.ledger.com/uploads/2026/03/hero_visual-1.webp';
+
+const baseArgs = {
+ imageUrl: EXAMPLE_SRC,
+ onPress: () => ({}),
+ onClose: () => ({}),
+};
+
+export const Base: Story = {
+ args: baseArgs,
+ render: (args) => (
+
+
+
+
+ Black Friday sale.
+
+ {' '}
+ 3 days with no fees{' '}
+
+ on your transactions.
+
+
+
+ ),
+ parameters: {
+ docs: {
+ source: {
+ code: `
+ {}} onClose={() => {}}>
+
+
+ Black Friday sale.
+
+ {' '}
+ 3 days with no fees{' '}
+
+ on your transactions.
+
+`,
+ },
+ },
+ },
+};
+
+export const LayoutShowcase: Story = {
+ render: () => (
+
+
+
+ Full width card
+
+
+ ),
+ parameters: {
+ docs: {
+ source: {
+ code: `
+ {}} onClose={() => {}}>
+
+ Full width card
+`,
+ },
+ },
+ },
+};
+
+export const CompositionShowcase: Story = {
+ render: () => (
+
+
+ Title only
+
+
+
+
+ With tag and title
+
+
+
+
+ With crypto icon
+
+
+ ),
+ parameters: {
+ docs: {
+ source: {
+ code: `
+{/* Title only */}
+ {}} onClose={() => {}}>
+ Title only
+
+
+{/* With tag */}
+ {}} onClose={() => {}}>
+
+ With tag and title
+
+
+{/* With crypto icon */}
+ {}} onClose={() => {}}>
+
+ With crypto icon
+`,
+ },
+ },
+ },
+};
+
+export const WithClose: Story = {
+ render: () => {
+ const [visible, setVisible] = useState(true);
+
+ if (!visible) {
+ return (
+
+ );
+ }
+
+ return (
+
+ ({})}
+ onClose={() => setVisible(false)}
+ >
+
+
+ Black Friday sale.
+
+ {' '}
+ 3 days with no fees{' '}
+
+ on your transactions.
+
+
+
+ );
+ },
+};
diff --git a/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.test.tsx b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.test.tsx
new file mode 100644
index 000000000..f9b67239a
--- /dev/null
+++ b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.test.tsx
@@ -0,0 +1,112 @@
+import { describe, it, expect, jest } from '@jest/globals';
+import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
+import { fireEvent, render } from '@testing-library/react-native';
+import type { ReactNode } from 'react';
+import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
+import { MediaCard, MediaCardTitle } from './MediaCard';
+
+const TestWrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
+
+const makeProps = () => ({
+ imageUrl: 'https://example.com/image.jpg',
+ onPress: jest.fn(),
+ onClose: jest.fn(),
+});
+
+describe('MediaCard', () => {
+ it('should render title', () => {
+ const { getByText } = render(
+
+
+ Title
+
+ ,
+ );
+
+ getByText('Title');
+ });
+
+ it('should render leading content and title', () => {
+ const { getByText } = render(
+
+
+ Tag
+ Title
+
+ ,
+ );
+
+ getByText('Tag');
+ getByText('Title');
+ });
+
+ it('should fire onPress on press', () => {
+ const props = makeProps();
+ const { getByTestId } = render(
+
+
+ Title
+
+ ,
+ );
+
+ fireEvent.press(getByTestId('media-card'));
+ expect(props.onPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('should show image after successful load', () => {
+ const { getByTestId } = render(
+
+
+ Title
+
+ ,
+ );
+
+ const img = getByTestId('media-card-image');
+ expect(img.props.style).toEqual(
+ expect.arrayContaining([expect.objectContaining({ opacity: 0 })]),
+ );
+
+ fireEvent(img, 'onLoad');
+ expect(img.props.style).not.toEqual(
+ expect.arrayContaining([expect.objectContaining({ opacity: 0 })]),
+ );
+ });
+
+ it('should hide image on error', () => {
+ const { getByTestId } = render(
+
+
+ Title
+
+ ,
+ );
+
+ const img = getByTestId('media-card-image');
+ fireEvent(img, 'onLoad');
+ fireEvent(img, 'onError');
+
+ expect(img.props.style).toEqual(
+ expect.arrayContaining([expect.objectContaining({ opacity: 0 })]),
+ );
+ });
+
+ it('should fire onClose on close button press', () => {
+ const props = makeProps();
+ const { getByTestId } = render(
+
+
+ Title
+
+ ,
+ );
+
+ fireEvent.press(getByTestId('media-card-close-button'));
+ expect(props.onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.tsx b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.tsx
new file mode 100644
index 000000000..a4c817463
--- /dev/null
+++ b/libs/ui-rnative/src/lib/Components/MediaCard/MediaCard.tsx
@@ -0,0 +1,168 @@
+import { useState } from 'react';
+import { Image, StyleSheet, View } from 'react-native';
+import { useCommonTranslation } from '../../../i18n';
+import { useStyleSheet } from '../../../styles';
+import { Close } from '../../Symbols';
+import { InteractiveIcon } from '../InteractiveIcon';
+import { LinearGradient, Pressable, Text } from '../Utility';
+import { MediaCardProps, MediaCardTitleProps } from './types';
+
+const CARD_HEIGHT = 164;
+
+const useStyles = () =>
+ useStyleSheet(
+ (t) => ({
+ root: {
+ position: 'relative',
+ width: t.sizes.full,
+ height: CARD_HEIGHT,
+ borderRadius: t.borderRadius.md,
+ overflow: 'hidden',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ justifyContent: 'flex-end',
+ backgroundColor: t.colors.bg.muted,
+ },
+ content: {
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: t.spacings.s8,
+ width: t.sizes.full,
+ minWidth: 0,
+ padding: t.spacings.s12,
+ },
+ title: {
+ ...t.typographies.heading3SemiBold,
+ color: t.colors.text.white,
+ },
+ closeButton: {
+ position: 'absolute',
+ top: t.spacings.s12,
+ right: t.spacings.s12,
+ },
+ }),
+ [],
+ );
+
+/**
+ * Title text for the card, styled with heading typography and white color.
+ */
+export const MediaCardTitle = ({
+ children,
+ lx = {},
+ style,
+ ref,
+ ...props
+}: MediaCardTitleProps) => {
+ const styles = useStyles();
+
+ return (
+
+ {children}
+
+ );
+};
+
+MediaCardTitle.displayName = 'MediaCardTitle';
+
+const GradientOverlays = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+/**
+ * A media card component for displaying a full-bleed background image with
+ * composable content and a close button, using gradient overlays to ensure
+ * readability.
+ *
+ * @example
+ * import { MediaCard, MediaCardTitle } from '@ledgerhq/lumen-ui-rnative';
+ * import { Tag } from '@ledgerhq/lumen-ui-rnative';
+ *
+ * {}} onClose={() => {}}>
+ *
+ * Card title
+ *
+ */
+export const MediaCard = ({
+ ref,
+ children,
+ imageUrl,
+ onPress,
+ onClose,
+ lx = {},
+ style,
+ ...pressableProps
+}: MediaCardProps) => {
+ const { t } = useCommonTranslation();
+ const [imageLoaded, setImageLoaded] = useState(false);
+
+ const styles = useStyles();
+
+ return (
+
+ setImageLoaded(true)}
+ onError={() => setImageLoaded(false)}
+ testID='media-card-image'
+ />
+
+ {imageLoaded && }
+
+ {children}
+
+
+
+
+
+ );
+};
+
+MediaCard.displayName = 'MediaCard';
diff --git a/libs/ui-rnative/src/lib/Components/MediaCard/index.ts b/libs/ui-rnative/src/lib/Components/MediaCard/index.ts
new file mode 100644
index 000000000..804bd2382
--- /dev/null
+++ b/libs/ui-rnative/src/lib/Components/MediaCard/index.ts
@@ -0,0 +1,2 @@
+export { MediaCard, MediaCardTitle } from './MediaCard';
+export * from './types';
diff --git a/libs/ui-rnative/src/lib/Components/MediaCard/types.ts b/libs/ui-rnative/src/lib/Components/MediaCard/types.ts
new file mode 100644
index 000000000..f7d2237f5
--- /dev/null
+++ b/libs/ui-rnative/src/lib/Components/MediaCard/types.ts
@@ -0,0 +1,35 @@
+import type { ReactNode } from 'react';
+import { StyledPressableProps, StyledTextProps } from '../../../styles';
+
+/**
+ * Props for the MediaCard root component
+ */
+export type MediaCardProps = {
+ /**
+ * The source URL for the background image.
+ */
+ imageUrl: string;
+ /**
+ * Callback fired when the card is pressed.
+ */
+ onPress: () => void;
+ /**
+ * Callback fired when the close button is pressed.
+ */
+ onClose: () => void;
+ /**
+ * The card content — typically a `MediaCardTitle` and optional
+ * leading content such as tags or icons.
+ */
+ children: ReactNode;
+} & Omit;
+
+/**
+ * Props for the MediaCardTitle component
+ */
+export type MediaCardTitleProps = {
+ /**
+ * The title text or custom content.
+ */
+ children: ReactNode;
+} & Omit;
diff --git a/libs/ui-rnative/src/lib/Components/index.ts b/libs/ui-rnative/src/lib/Components/index.ts
index c394456f5..5ed1e0b59 100644
--- a/libs/ui-rnative/src/lib/Components/index.ts
+++ b/libs/ui-rnative/src/lib/Components/index.ts
@@ -15,6 +15,7 @@ export * from './IconButton';
export * from './InteractiveIcon';
export * from './Link';
export * from './ListItem';
+export * from './MediaCard';
export * from './NavBar';
export * from './PageIndicator';
export * from './SearchInput';