From befddee705ddeb9c2f7775480249852eb516a7d4 Mon Sep 17 00:00:00 2001
From: georgewrmarshall <george.marshall@consensys.net>
Date: Thu, 30 Jan 2025 16:07:07 -0800
Subject: [PATCH] chore: adding initial avatar network component

---
 .../avatar-base/AvatarBase.constants.ts       |  12 +-
 .../avatar-base/AvatarBase.stories.tsx        |  93 +++++++++----
 .../avatar-base/AvatarBase.test.tsx           |  36 +++--
 .../src/components/avatar-base/AvatarBase.tsx |  21 ++-
 .../avatar-base/AvatarBase.types.ts           |  16 +++
 .../src/components/avatar-base/README.mdx     |  27 +++-
 .../src/components/avatar-base/index.ts       |   3 +
 .../avatar-network/AvatarNetwork.constants.ts |  16 +++
 .../avatar-network/AvatarNetwork.stories.tsx  | 113 ++++++++++++++++
 .../avatar-network/AvatarNetwork.test.tsx     | 127 ++++++++++++++++++
 .../avatar-network/AvatarNetwork.tsx          |  53 ++++++++
 .../avatar-network/AvatarNetwork.types.ts     |  45 +++++++
 .../src/components/avatar-network/README.mdx  |  88 ++++++++++++
 .../src/components/avatar-network/index.ts    |   3 +
 .../src/components/index.ts                   |   8 ++
 .../src/components/text/Text.test.tsx         |   6 +-
 .../src/components/text/Text.tsx              |   3 +-
 17 files changed, 608 insertions(+), 62 deletions(-)
 create mode 100644 packages/design-system-react/src/components/avatar-base/index.ts
 create mode 100644 packages/design-system-react/src/components/avatar-network/AvatarNetwork.constants.ts
 create mode 100644 packages/design-system-react/src/components/avatar-network/AvatarNetwork.stories.tsx
 create mode 100644 packages/design-system-react/src/components/avatar-network/AvatarNetwork.test.tsx
 create mode 100644 packages/design-system-react/src/components/avatar-network/AvatarNetwork.tsx
 create mode 100644 packages/design-system-react/src/components/avatar-network/AvatarNetwork.types.ts
 create mode 100644 packages/design-system-react/src/components/avatar-network/README.mdx
 create mode 100644 packages/design-system-react/src/components/avatar-network/index.ts

diff --git a/packages/design-system-react/src/components/avatar-base/AvatarBase.constants.ts b/packages/design-system-react/src/components/avatar-base/AvatarBase.constants.ts
index ecee4674..031fe76a 100644
--- a/packages/design-system-react/src/components/avatar-base/AvatarBase.constants.ts
+++ b/packages/design-system-react/src/components/avatar-base/AvatarBase.constants.ts
@@ -1,9 +1,9 @@
 import { AvatarBaseSize } from './AvatarBase.types';
 
-export const AVATAR_BASE_SIZE_DIMENSIONS: Record<AvatarBaseSize, string> = {
-  [AvatarBaseSize.Xs]: 'h-4 w-4 text-s-body-xs',
-  [AvatarBaseSize.Sm]: 'h-6 w-6 text-s-body-sm',
-  [AvatarBaseSize.Md]: 'h-8 w-8 text-s-body-md',
-  [AvatarBaseSize.Lg]: 'h-10 w-10 text-s-body-lg',
-  [AvatarBaseSize.Xl]: 'h-12 w-12 text-s-body-lg',
+export const AVATAR_BASE_SIZE_CLASS_MAP: Record<AvatarBaseSize, string> = {
+  [AvatarBaseSize.Xs]: 'h-4 w-4',
+  [AvatarBaseSize.Sm]: 'h-6 w-6',
+  [AvatarBaseSize.Md]: 'h-8 w-8',
+  [AvatarBaseSize.Lg]: 'h-10 w-10',
+  [AvatarBaseSize.Xl]: 'h-12 w-12',
 };
diff --git a/packages/design-system-react/src/components/avatar-base/AvatarBase.stories.tsx b/packages/design-system-react/src/components/avatar-base/AvatarBase.stories.tsx
index f227f534..1f677578 100644
--- a/packages/design-system-react/src/components/avatar-base/AvatarBase.stories.tsx
+++ b/packages/design-system-react/src/components/avatar-base/AvatarBase.stories.tsx
@@ -1,9 +1,9 @@
 import type { Meta, StoryObj } from '@storybook/react';
 import React from 'react';
 
-import { Icon, IconName, IconSize } from '..';
+import { Icon, IconName, IconSize, Text, TextVariant, TextColor } from '..';
 import { AvatarBase } from './AvatarBase';
-import { AvatarBaseSize } from './AvatarBase.types';
+import { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types';
 import README from './README.mdx';
 
 const meta: Meta<typeof AvatarBase> = {
@@ -31,6 +31,12 @@ const meta: Meta<typeof AvatarBase> = {
       mapping: AvatarBaseSize,
       description: 'Optional prop to control the size of the AvatarBase',
     },
+    shape: {
+      control: 'select',
+      options: Object.keys(AvatarBaseShape),
+      mapping: AvatarBaseShape,
+      description: 'Optional prop to control the shape of the AvatarBase',
+    },
   },
 };
 
@@ -38,19 +44,66 @@ export default meta;
 type Story = StoryObj<typeof AvatarBase>;
 
 export const Default: Story = {
+  render: (args) => (
+    <AvatarBase {...args}>
+      <Text>{args.children}</Text>
+    </AvatarBase>
+  ),
   args: {
     children: 'A',
   },
 };
 
-export const Size: Story = {
+export const Shape: Story = {
   render: () => (
     <div className="flex gap-2 items-center">
-      <AvatarBase size={AvatarBaseSize.Xs}>Xs</AvatarBase>
-      <AvatarBase size={AvatarBaseSize.Sm}>Sm</AvatarBase>
-      <AvatarBase size={AvatarBaseSize.Md}>Md</AvatarBase>
-      <AvatarBase size={AvatarBaseSize.Lg}>Lg</AvatarBase>
-      <AvatarBase size={AvatarBaseSize.Xl}>Xl</AvatarBase>
+      <AvatarBase shape={AvatarBaseShape.Circle}>
+        <Text variant={TextVariant.BodySm}>C</Text>
+      </AvatarBase>
+      <AvatarBase shape={AvatarBaseShape.Square}>
+        <Text variant={TextVariant.BodySm}>S</Text>
+      </AvatarBase>
+    </div>
+  ),
+};
+
+export const Size: Story = {
+  render: () => (
+    <div className="flex flex-col gap-2">
+      <div className="flex gap-2 items-center">
+        <AvatarBase size={AvatarBaseSize.Xs}>
+          <Text variant={TextVariant.BodyXs}>Xs</Text>
+        </AvatarBase>
+        <AvatarBase size={AvatarBaseSize.Sm}>
+          <Text variant={TextVariant.BodyXs}>Sm</Text>
+        </AvatarBase>
+        <AvatarBase size={AvatarBaseSize.Md}>
+          <Text variant={TextVariant.BodySm}>Md</Text>
+        </AvatarBase>
+        <AvatarBase size={AvatarBaseSize.Lg}>
+          <Text variant={TextVariant.BodyMd}>Lg</Text>
+        </AvatarBase>
+        <AvatarBase size={AvatarBaseSize.Xl}>
+          <Text variant={TextVariant.BodyMd}>Xl</Text>
+        </AvatarBase>
+      </div>
+      <div className="flex gap-2 items-center">
+        <AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Xs}>
+          <Text variant={TextVariant.BodyXs}>Xs</Text>
+        </AvatarBase>
+        <AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Sm}>
+          <Text variant={TextVariant.BodyXs}>Sm</Text>
+        </AvatarBase>
+        <AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Md}>
+          <Text variant={TextVariant.BodySm}>Md</Text>
+        </AvatarBase>
+        <AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Lg}>
+          <Text variant={TextVariant.BodyMd}>Lg</Text>
+        </AvatarBase>
+        <AvatarBase shape={AvatarBaseShape.Square} size={AvatarBaseSize.Xl}>
+          <Text variant={TextVariant.BodyMd}>Xl</Text>
+        </AvatarBase>
+      </div>
     </div>
   ),
 };
@@ -59,7 +112,9 @@ export const Children: Story = {
   render: () => (
     <div className="flex gap-2 items-center">
       {/* Text */}
-      <AvatarBase>A</AvatarBase>
+      <AvatarBase>
+        <Text>A</Text>
+      </AvatarBase>
       {/* Image */}
       <AvatarBase>
         <img
@@ -70,25 +125,7 @@ export const Children: Story = {
       </AvatarBase>
       {/* Icon */}
       <AvatarBase>
-        <Icon
-          name={IconName.User}
-          size={IconSize.Sm}
-          className="text-inherit"
-        />
-      </AvatarBase>
-    </div>
-  ),
-};
-
-export const ClassName: Story = {
-  render: () => (
-    <div className="flex gap-2">
-      <AvatarBase className="bg-success-default text-success-inverse">
-        S
-      </AvatarBase>
-      <AvatarBase className="bg-error-default text-error-inverse">E</AvatarBase>
-      <AvatarBase className="bg-warning-default text-warning-inverse">
-        W
+        <Icon name={IconName.User} size={IconSize.Sm} />
       </AvatarBase>
     </div>
   ),
diff --git a/packages/design-system-react/src/components/avatar-base/AvatarBase.test.tsx b/packages/design-system-react/src/components/avatar-base/AvatarBase.test.tsx
index a9cb8d51..cda65dd1 100644
--- a/packages/design-system-react/src/components/avatar-base/AvatarBase.test.tsx
+++ b/packages/design-system-react/src/components/avatar-base/AvatarBase.test.tsx
@@ -2,23 +2,15 @@ import { render, screen } from '@testing-library/react';
 import React from 'react';
 
 import { AvatarBase } from './AvatarBase';
-import { AVATAR_BASE_SIZE_DIMENSIONS } from './AvatarBase.constants';
-import { AvatarBaseSize } from './AvatarBase.types';
+import { AVATAR_BASE_SIZE_CLASS_MAP } from './AvatarBase.constants';
+import { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types';
 
 describe('AvatarBase', () => {
   it('renders with default styles', () => {
     render(<AvatarBase>A</AvatarBase>);
 
     const avatar = screen.getByText('A');
-    expect(avatar).toHaveClass(
-      'inline-flex',
-      'items-center',
-      'justify-center',
-      'rounded-full',
-      'bg-background-alternative',
-      'text-default',
-      'uppercase',
-    );
+    expect(avatar).toBeInTheDocument();
   });
 
   it('applies size classes correctly', () => {
@@ -26,7 +18,7 @@ describe('AvatarBase', () => {
       <AvatarBase size={AvatarBaseSize.Xs}>A</AvatarBase>,
     );
 
-    Object.entries(AVATAR_BASE_SIZE_DIMENSIONS).forEach(([size, classes]) => {
+    Object.entries(AVATAR_BASE_SIZE_CLASS_MAP).forEach(([size, classes]) => {
       rerender(<AvatarBase size={size as AvatarBaseSize}>A</AvatarBase>);
       const avatar = screen.getByText('A');
       const classArray = classes.split(' ');
@@ -71,4 +63,24 @@ describe('AvatarBase', () => {
     const avatar = screen.getByText('A');
     expect(avatar).toHaveStyle({ backgroundColor: 'red' });
   });
+
+  it('applies correct shape classes', () => {
+    const { rerender } = render(
+      <AvatarBase shape={AvatarBaseShape.Circle}>A</AvatarBase>,
+    );
+
+    let avatar = screen.getByText('A');
+    expect(avatar).toHaveClass('rounded-full');
+
+    rerender(<AvatarBase shape={AvatarBaseShape.Square}>A</AvatarBase>);
+    avatar = screen.getByText('A');
+    expect(avatar).toHaveClass('rounded-lg');
+  });
+
+  it('uses circle shape by default', () => {
+    render(<AvatarBase>A</AvatarBase>);
+
+    const avatar = screen.getByText('A');
+    expect(avatar).toHaveClass('rounded-full');
+  });
 });
diff --git a/packages/design-system-react/src/components/avatar-base/AvatarBase.tsx b/packages/design-system-react/src/components/avatar-base/AvatarBase.tsx
index fceb953d..0aa79077 100644
--- a/packages/design-system-react/src/components/avatar-base/AvatarBase.tsx
+++ b/packages/design-system-react/src/components/avatar-base/AvatarBase.tsx
@@ -2,13 +2,21 @@ import { Slot } from '@radix-ui/react-slot';
 import React from 'react';
 
 import { twMerge } from '../../utils/tw-merge';
-import { AVATAR_BASE_SIZE_DIMENSIONS } from './AvatarBase.constants';
+import { AVATAR_BASE_SIZE_CLASS_MAP } from './AvatarBase.constants';
 import type { AvatarBaseProps } from './AvatarBase.types';
-import { AvatarBaseSize } from './AvatarBase.types';
+import { AvatarBaseShape, AvatarBaseSize } from './AvatarBase.types';
 
 export const AvatarBase = React.forwardRef<HTMLDivElement, AvatarBaseProps>(
   (
-    { children, className, size = AvatarBaseSize.Md, asChild, style, ...props },
+    {
+      children,
+      className,
+      size = AvatarBaseSize.Md,
+      shape = AvatarBaseShape.Circle,
+      asChild,
+      style,
+      ...props
+    },
     ref,
   ) => {
     const Component = asChild ? Slot : 'div';
@@ -16,12 +24,11 @@ export const AvatarBase = React.forwardRef<HTMLDivElement, AvatarBaseProps>(
     const mergedClassName = twMerge(
       // Base styles
       'inline-flex items-center justify-center',
-      'rounded-full',
-      'bg-background-alternative',
-      'text-default uppercase font-medium',
+      shape === AvatarBaseShape.Circle ? 'rounded-full' : 'rounded-lg',
+      'bg-alternative',
       'overflow-hidden',
       // Size
-      AVATAR_BASE_SIZE_DIMENSIONS[size],
+      AVATAR_BASE_SIZE_CLASS_MAP[size],
       // Custom classes
       className,
     );
diff --git a/packages/design-system-react/src/components/avatar-base/AvatarBase.types.ts b/packages/design-system-react/src/components/avatar-base/AvatarBase.types.ts
index 3120371d..692fca63 100644
--- a/packages/design-system-react/src/components/avatar-base/AvatarBase.types.ts
+++ b/packages/design-system-react/src/components/avatar-base/AvatarBase.types.ts
@@ -23,6 +23,17 @@ export enum AvatarBaseSize {
   Xl = 'xl',
 }
 
+export enum AvatarBaseShape {
+  /**
+   * Circular shape with fully rounded corners
+   */
+  Circle = 'circle',
+  /**
+   * Square shape with slight rounded corners
+   */
+  Square = 'square',
+}
+
 export type AvatarBaseProps = ComponentProps<'div'> & {
   /**
    * Required prop for the content to be rendered within the AvatarBase
@@ -49,4 +60,9 @@ export type AvatarBaseProps = ComponentProps<'div'> & {
    * Should be used sparingly and only for dynamic styles that can't be achieved with className.
    */
   style?: React.CSSProperties;
+  /**
+   * Optional prop to control the shape of the AvatarBase
+   * @default AvatarBaseShape.Circle
+   */
+  shape?: AvatarBaseShape;
 };
diff --git a/packages/design-system-react/src/components/avatar-base/README.mdx b/packages/design-system-react/src/components/avatar-base/README.mdx
index 12ed2c77..2db1b9aa 100644
--- a/packages/design-system-react/src/components/avatar-base/README.mdx
+++ b/packages/design-system-react/src/components/avatar-base/README.mdx
@@ -7,15 +7,26 @@ import * as AvatarBaseStories from './AvatarBase.stories';
 The AvatarBase is the base component for avatars
 
 ```tsx
-import { AvatarBase } from '@metamask/design-system-react';
+import { AvatarBase, Text } from '@metamask/design-system-react';
 
-<AvatarBase>A</AvatarBase>;
+<AvatarBase>
+  <Text>A</Text>
+</AvatarBase>;
 ```
 
 <Canvas of={AvatarBaseStories.Default} />
 
 ## Props
 
+### Shape
+
+AvatarBase supports two shapes:
+
+- `AvatarBaseShape.Circle` (fully rounded) - default
+- `AvatarBaseShape.Square` (slightly rounded corners)
+
+<Canvas of={AvatarBaseStories.Shape} />
+
 ### Size
 
 AvatarBase supports five sizes:
@@ -34,23 +45,25 @@ AvatarBase can contain different types of content including text, images, and ic
 
 <Canvas of={AvatarBaseStories.Children} />
 
-### ClassName
+### Class Name
 
 Use the `className` prop to add custom 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
 
-<Canvas of={AvatarBaseStories.ClassName} />
-
 Example:
 
 ```tsx
 // Adding new styles
-<AvatarBase className="my-4 mx-2">A</AvatarBase>
+<AvatarBase className="my-4 mx-2">
+  <Text>A</Text>
+</AvatarBase>
 
 // Overriding default styles
-<AvatarBase className="bg-success-default text-success-inverse">S</AvatarBase>
+<AvatarBase className="bg-success-default">
+  <Text color={TextColor.SuccessInverse}>A</Text>
+</AvatarBase>
 ```
 
 ### Style
diff --git a/packages/design-system-react/src/components/avatar-base/index.ts b/packages/design-system-react/src/components/avatar-base/index.ts
new file mode 100644
index 00000000..d86a8a1f
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-base/index.ts
@@ -0,0 +1,3 @@
+export { AvatarBase } from './AvatarBase';
+export type { AvatarBaseProps } from './AvatarBase.types';
+export { AvatarBaseSize, AvatarBaseShape } from './AvatarBase.types';
diff --git a/packages/design-system-react/src/components/avatar-network/AvatarNetwork.constants.ts b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.constants.ts
new file mode 100644
index 00000000..a0d030ec
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.constants.ts
@@ -0,0 +1,16 @@
+// Remove this file if it's not needed
+export const AVATARNETWORK_CLASSMAP = {};
+
+import { AvatarBaseSize } from '../avatar-base';
+import { TextVariant } from '../text';
+
+export const AVATAR_NETWORK_SIZE_TO_TEXT_VARIANT_MAP: Record<
+  AvatarBaseSize,
+  TextVariant
+> = {
+  [AvatarBaseSize.Xs]: TextVariant.BodyXs,
+  [AvatarBaseSize.Sm]: TextVariant.BodyXs,
+  [AvatarBaseSize.Md]: TextVariant.BodySm,
+  [AvatarBaseSize.Lg]: TextVariant.BodyMd,
+  [AvatarBaseSize.Xl]: TextVariant.BodyMd,
+};
diff --git a/packages/design-system-react/src/components/avatar-network/AvatarNetwork.stories.tsx b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.stories.tsx
new file mode 100644
index 00000000..b30fdff7
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.stories.tsx
@@ -0,0 +1,113 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+
+import { TextColor } from '..';
+import { AvatarNetwork } from './AvatarNetwork';
+import { AvatarNetworkSize } from '.';
+import README from './README.mdx';
+
+const meta: Meta<typeof AvatarNetwork> = {
+  title: 'React Components/AvatarNetwork',
+  component: AvatarNetwork,
+  parameters: {
+    docs: {
+      page: README,
+    },
+  },
+  argTypes: {
+    src: {
+      control: 'text',
+      description:
+        'Optional URL for the network image. When provided, displays the image instead of fallback text',
+    },
+    size: {
+      control: 'select',
+      options: Object.keys(AvatarNetworkSize),
+      mapping: AvatarNetworkSize,
+      description:
+        'Optional prop to control the size of the avatar. Defaults to AvatarNetworkSize.Md',
+    },
+    fallbackText: {
+      control: 'text',
+      description:
+        'Required text to display when no image is provided. Also used as alt text for the image when src is provided',
+    },
+    fallbackTextProps: {
+      control: 'object',
+      description:
+        'Optional props to be passed to the Text component when rendering fallback text. Only used when src is not provided',
+    },
+    className: {
+      control: 'text',
+      description:
+        'Optional additional CSS classes to be applied to the component',
+    },
+  },
+};
+
+export default meta;
+type Story = StoryObj<typeof AvatarNetwork>;
+
+export const Default: Story = {
+  args: {
+    src: 'https://cryptologos.cc/logos/ethereum-eth-logo.png',
+    fallbackText: 'Eth',
+  },
+};
+
+export const Src: Story = {
+  render: () => (
+    <div className="flex gap-2">
+      <AvatarNetwork
+        fallbackText="Eth"
+        src="https://cryptologos.cc/logos/ethereum-eth-logo.png"
+      />
+      <AvatarNetwork
+        fallbackText="Ava"
+        src="https://cryptologos.cc/logos/avalanche-avax-logo.png"
+      />
+      <AvatarNetwork
+        fallbackText="Pol"
+        src="https://cryptologos.cc/logos/polygon-matic-logo.png"
+      />
+    </div>
+  ),
+};
+
+export const FallbackText: Story = {
+  render: () => (
+    <div className="flex gap-2">
+      <AvatarNetwork fallbackText="Eth" />
+      <AvatarNetwork fallbackText="Ava" />
+      <AvatarNetwork
+        fallbackText="Pol"
+        fallbackTextProps={{
+          color: TextColor.ErrorDefault,
+          'data-testid': 'fallback-text',
+        }}
+      />
+    </div>
+  ),
+};
+
+export const FallbackTextProps: Story = {
+  args: {
+    fallbackText: 'Eth',
+    fallbackTextProps: {
+      color: TextColor.ErrorDefault,
+      'data-testid': 'fallback-text',
+    },
+  },
+};
+
+export const Size: Story = {
+  render: () => (
+    <div className="flex gap-2 items-center">
+      <AvatarNetwork fallbackText="E" size={AvatarNetworkSize.Xs} />
+      <AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Sm} />
+      <AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Md} />
+      <AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Lg} />
+      <AvatarNetwork fallbackText="Eth" size={AvatarNetworkSize.Xl} />
+    </div>
+  ),
+};
diff --git a/packages/design-system-react/src/components/avatar-network/AvatarNetwork.test.tsx b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.test.tsx
new file mode 100644
index 00000000..410b8a3e
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.test.tsx
@@ -0,0 +1,127 @@
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+
+import { TextColor } from '..';
+import { AvatarNetwork } from './AvatarNetwork';
+import { AvatarNetworkSize } from '.';
+
+describe('AvatarNetwork', () => {
+  it('renders image when src is provided', () => {
+    render(<AvatarNetwork src="test-image.jpg" fallbackText="Eth" />);
+
+    const img = screen.getByRole('img');
+    expect(img).toBeInTheDocument();
+    expect(img).toHaveAttribute('src', 'test-image.jpg');
+    expect(img).toHaveAttribute('alt', 'Eth');
+  });
+
+  it('renders fallbackText when src is not provided', () => {
+    render(<AvatarNetwork fallbackText="Eth" />);
+    expect(screen.getByText('Eth')).toBeInTheDocument();
+  });
+
+  it('applies fallbackTextProps to Text component', () => {
+    render(
+      <AvatarNetwork
+        fallbackText="Eth"
+        fallbackTextProps={{
+          color: TextColor.TextAlternative,
+          className: 'test-class',
+          'data-testid': 'fallback-text',
+        }}
+      />,
+    );
+
+    const text = screen.getByTestId('fallback-text');
+    expect(text).toHaveClass('text-alternative', 'test-class');
+  });
+
+  it('applies custom className to root element', () => {
+    render(
+      <AvatarNetwork
+        fallbackText="Eth"
+        className="custom-class"
+        data-testid="avatar"
+      />,
+    );
+
+    const avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('custom-class');
+  });
+
+  it('passes through additional image props when src is provided', () => {
+    render(
+      <AvatarNetwork
+        src="test-image.jpg"
+        fallbackText="Eth"
+        imageProps={{
+          loading: 'lazy',
+        }}
+      />,
+    );
+
+    screen.debug();
+
+    const img = screen.getByRole('img');
+    expect(img).toHaveAttribute('loading', 'lazy');
+  });
+
+  it('applies size classes correctly', () => {
+    const { rerender } = render(
+      <AvatarNetwork
+        fallbackText="Eth"
+        size={AvatarNetworkSize.Xs}
+        data-testid="avatar"
+      />,
+    );
+
+    let avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('h-4 w-4');
+
+    rerender(
+      <AvatarNetwork
+        fallbackText="Eth"
+        size={AvatarNetworkSize.Sm}
+        data-testid="avatar"
+      />,
+    );
+    avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('h-6 w-6');
+
+    rerender(
+      <AvatarNetwork
+        fallbackText="Eth"
+        size={AvatarNetworkSize.Md}
+        data-testid="avatar"
+      />,
+    );
+    avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('h-8 w-8');
+
+    rerender(
+      <AvatarNetwork
+        fallbackText="Eth"
+        size={AvatarNetworkSize.Lg}
+        data-testid="avatar"
+      />,
+    );
+    avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('h-10 w-10');
+
+    rerender(
+      <AvatarNetwork
+        fallbackText="Eth"
+        size={AvatarNetworkSize.Xl}
+        data-testid="avatar"
+      />,
+    );
+    avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('h-12 w-12');
+  });
+
+  it('uses medium size by default', () => {
+    render(<AvatarNetwork fallbackText="Eth" data-testid="avatar" />);
+    const avatar = screen.getByTestId('avatar');
+    expect(avatar).toHaveClass('h-8 w-8');
+  });
+});
diff --git a/packages/design-system-react/src/components/avatar-network/AvatarNetwork.tsx b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.tsx
new file mode 100644
index 00000000..cc289fa2
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import { AvatarBase, AvatarBaseShape, AvatarBaseSize } from '../avatar-base';
+import { Text } from '../text';
+import { AVATAR_NETWORK_SIZE_TO_TEXT_VARIANT_MAP } from './AvatarNetwork.constants';
+import type { AvatarNetworkProps } from './AvatarNetwork.types';
+
+export const AvatarNetwork = React.forwardRef<
+  HTMLDivElement,
+  AvatarNetworkProps
+>(
+  (
+    {
+      src,
+      fallbackText,
+      fallbackTextProps,
+      className,
+      size = AvatarBaseSize.Md,
+      'data-testid': dataTestId,
+      imageProps,
+      ...props
+    },
+    ref,
+  ) => (
+    <AvatarBase
+      ref={ref}
+      shape={AvatarBaseShape.Square}
+      size={size}
+      className={className}
+      data-testid={dataTestId}
+      {...props}
+    >
+      {src ? (
+        <img
+          src={src}
+          alt={fallbackText}
+          className="w-full h-full object-cover"
+          {...imageProps}
+        />
+      ) : (
+        <Text
+          variant={AVATAR_NETWORK_SIZE_TO_TEXT_VARIANT_MAP[size]}
+          asChild
+          {...fallbackTextProps}
+        >
+          <span>{fallbackText}</span>
+        </Text>
+      )}
+    </AvatarBase>
+  ),
+);
+
+AvatarNetwork.displayName = 'AvatarNetwork';
diff --git a/packages/design-system-react/src/components/avatar-network/AvatarNetwork.types.ts b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.types.ts
new file mode 100644
index 00000000..5aea19a8
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/AvatarNetwork.types.ts
@@ -0,0 +1,45 @@
+import type { ComponentProps } from 'react';
+
+import type { TextProps } from '../text';
+import { AvatarNetworkSize } from '.';
+
+export type AvatarNetworkProps = Omit<
+  ComponentProps<'img'>,
+  'children' | 'size'
+> & {
+  /**
+   * Optional URL for the network image
+   * When provided, displays the image instead of fallback text
+   */
+  src?: string;
+  /**
+   * Optional prop to pass to the underlying img element
+   */
+  imageProps?: ComponentProps<'img'>;
+  /**
+   * Optional prop to control the size of the avatar
+   * @default AvatarNetworkSize.Md
+   */
+  size?: AvatarNetworkSize;
+  /**
+   * Required text to display when no image is provided
+   * Also used as alt text for the image when src is provided
+   */
+  fallbackText: string;
+  /**
+   * Optional props to be passed to the Text component when rendering fallback text
+   * Only used when src is not provided
+   */
+  fallbackTextProps?: Partial<
+    React.HTMLAttributes<HTMLSpanElement> & TextProps
+  >;
+  /**
+   * Optional additional CSS classes to be applied to the component
+   */
+  className?: string;
+  /**
+   * Optional prop for testing purposes
+   * Passed to the root element
+   */
+  'data-testid'?: string;
+};
diff --git a/packages/design-system-react/src/components/avatar-network/README.mdx b/packages/design-system-react/src/components/avatar-network/README.mdx
new file mode 100644
index 00000000..0b06d7b1
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/README.mdx
@@ -0,0 +1,88 @@
+import { Controls, Canvas } from '@storybook/blocks';
+
+import * as AvatarNetworkStories from './AvatarNetwork.stories';
+
+# AvatarNetwork
+
+AvatarNetwork is a component for displaying network avatars. It can show either a network's logo image or a fallback with the first letter of the network's name.
+
+```tsx
+import { AvatarNetwork } from '@metamask/design-system-react';
+
+<AvatarNetwork fallbackText="Eth" src="path/to/ethereum-logo.png" />;
+```
+
+<Canvas of={AvatarNetworkStories.Default} />
+
+## Props
+
+### Src (image source)
+
+The `src` prop is optional and specifies the URL of the network's logo image.
+
+<Canvas of={AvatarNetworkStories.Src} />
+
+### Image Props
+
+All standard HTML img attributes are supported and will be passed to the underlying img element when `src` is provided.
+
+```tsx
+<AvatarNetwork
+  src="path/to/ethereum-logo.png"
+  fallbackText="Eth"
+  imageProps={{ alt: 'Ethereum', loading: 'lazy' }}
+/>
+```
+
+### Fallback Text
+
+The `fallbackText` prop is required and serves two purposes:
+
+- Alt text for the network image when `src` is provided. For better accessibility, it's recommended to use the imageProps `alt` attribute.
+- Fallback display text shows when `src` is not provided. It will show the entire string
+
+<Canvas of={AvatarNetworkStories.FallbackText} />
+
+### Fallback Text Props
+
+The `fallbackTextProps` prop allows you to customize the Text component used for the fallback display:
+
+<Canvas of={AvatarNetworkStories.FallbackTextProps} />
+
+### Size
+
+AvatarNetwork supports five sizes, each with a corresponding text variant for the fallback text:
+
+- `AvatarNetworkSize.Xs` (16px) - uses TextVariant.BodyXs
+- `AvatarNetworkSize.Sm` (24px) - uses TextVariant.BodyXs
+- `AvatarNetworkSize.Md` (32px) - uses TextVariant.BodySm (default)
+- `AvatarNetworkSize.Lg` (40px) - uses TextVariant.BodyMd
+- `AvatarNetworkSize.Xl` (48px) - uses TextVariant.BodyMd
+
+<Canvas of={AvatarNetworkStories.Size} />
+
+### Class Name
+
+Use the `className` prop to add custom 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
+
+Example:
+
+```tsx
+// Adding new styles
+<AvatarNetwork
+  src="path/to/ethereum-logo.png"
+  fallbackText="Eth"
+  className="my-4 mx-2"
+/>
+```
+
+## Component API
+
+<Controls of={AvatarNetworkStories.Default} />
+
+## References
+
+[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940)
diff --git a/packages/design-system-react/src/components/avatar-network/index.ts b/packages/design-system-react/src/components/avatar-network/index.ts
new file mode 100644
index 00000000..59f3ad36
--- /dev/null
+++ b/packages/design-system-react/src/components/avatar-network/index.ts
@@ -0,0 +1,3 @@
+export { AvatarNetwork } from './AvatarNetwork';
+export type { AvatarNetworkProps } from './AvatarNetwork.types';
+export { AvatarBaseSize as AvatarNetworkSize } from '../avatar-base';
diff --git a/packages/design-system-react/src/components/index.ts b/packages/design-system-react/src/components/index.ts
index 1ba7dc5a..9efc7e02 100644
--- a/packages/design-system-react/src/components/index.ts
+++ b/packages/design-system-react/src/components/index.ts
@@ -32,3 +32,11 @@ export type { ButtonProps } from './button';
 
 export { TextButton } from './text-button';
 export type { TextButtonProps } from './text-button';
+
+export { AvatarBase } from './avatar-base';
+export type { AvatarBaseProps } from './avatar-base';
+export { AvatarBaseSize, AvatarBaseShape } from './avatar-base';
+
+export { AvatarNetwork } from './avatar-network';
+export { AvatarNetworkSize } from './avatar-network';
+export type { AvatarNetworkProps } from './avatar-network';
diff --git a/packages/design-system-react/src/components/text/Text.test.tsx b/packages/design-system-react/src/components/text/Text.test.tsx
index 5e3bb1a5..20b64942 100644
--- a/packages/design-system-react/src/components/text/Text.test.tsx
+++ b/packages/design-system-react/src/components/text/Text.test.tsx
@@ -15,7 +15,11 @@ import { TEXT_CLASS_MAP } from './Text.constants';
 
 describe('Text Component', () => {
   it('renders children correctly', () => {
-    render(<Text variant={TextVariant.BodyMd}>Hello, World!</Text>);
+    render(
+      <Text variant={TextVariant.BodyMd} data-testid="text">
+        Hello, World!
+      </Text>,
+    );
     expect(screen.getByText('Hello, World!')).toBeInTheDocument();
   });
 
diff --git a/packages/design-system-react/src/components/text/Text.tsx b/packages/design-system-react/src/components/text/Text.tsx
index c1a15526..c8fbe892 100644
--- a/packages/design-system-react/src/components/text/Text.tsx
+++ b/packages/design-system-react/src/components/text/Text.tsx
@@ -19,6 +19,7 @@ export const Text: React.FC<TextProps> = ({
   asChild,
   color = TextColor.TextDefault,
   style,
+  ...props
 }) => {
   // When asChild is true, use Radix Slot to merge props onto the child component.
   // Otherwise, render the semantic HTML element mapped to this variant (e.g. h1-h4, p).
@@ -37,7 +38,7 @@ export const Text: React.FC<TextProps> = ({
   );
 
   return (
-    <Component className={mergedClassName} style={style}>
+    <Component className={mergedClassName} style={style} {...props}>
       {children}
     </Component>
   );