diff --git a/.changeset/clever-carrots-march.md b/.changeset/clever-carrots-march.md
new file mode 100644
index 00000000000..e85a2296560
--- /dev/null
+++ b/.changeset/clever-carrots-march.md
@@ -0,0 +1,5 @@
+---
+"thirdweb": minor
+---
+
+Add headless components: ChainProvider, ChainIcon & ChainName
diff --git a/apps/portal/src/app/react/v5/components/onchain/page.mdx b/apps/portal/src/app/react/v5/components/onchain/page.mdx
index fc36af804d2..17c72de5d27 100644
--- a/apps/portal/src/app/react/v5/components/onchain/page.mdx
+++ b/apps/portal/src/app/react/v5/components/onchain/page.mdx
@@ -113,4 +113,27 @@ Build your own UI and interact with onchain data using headless components.
description="Component to display the description of an NFT"
/>
+### Chains
+
+
+
+
+
+
+
diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts
index c41928f6f42..65496c90206 100644
--- a/packages/thirdweb/src/exports/react.ts
+++ b/packages/thirdweb/src/exports/react.ts
@@ -253,3 +253,17 @@ export {
TokenIcon,
type TokenIconProps,
} from "../react/web/ui/prebuilt/Token/icon.js";
+
+// Chain
+export {
+ ChainProvider,
+ type ChainProviderProps,
+} from "../react/web/ui/prebuilt/Chain/provider.js";
+export {
+ ChainName,
+ type ChainNameProps,
+} from "../react/web/ui/prebuilt/Chain/name.js";
+export {
+ ChainIcon,
+ type ChainIconProps,
+} from "../react/web/ui/prebuilt/Chain/icon.js";
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
new file mode 100644
index 00000000000..356fe69b58b
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/icon.tsx
@@ -0,0 +1,154 @@
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type { JSX } from "react";
+import { getChainMetadata } from "../../../../../chains/utils.js";
+import type { ThirdwebClient } from "../../../../../client/client.js";
+import { resolveScheme } from "../../../../../utils/ipfs.js";
+import { useChainContext } from "./provider.js";
+
+/**
+ * Props for the ChainIcon component
+ * @chain
+ * @component
+ */
+export interface ChainIconProps
+ extends Omit, "src"> {
+ /**
+ * You need a ThirdwebClient to resolve the icon which is hosted on IPFS.
+ * (since most chain icons are hosted on IPFS, loading them via thirdweb gateway will ensure better performance)
+ */
+ client: ThirdwebClient;
+ /**
+ * This prop can be a string or a (async) function that resolves to a string, representing the icon url of the chain
+ * This is particularly useful if you already have a way to fetch the chain icon.
+ */
+ iconResolver?: string | (() => string) | (() => Promise);
+ /**
+ * This component will be shown while the avatar of the icon is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: JSX.Element;
+ /**
+ * This component will be shown if the request for fetching the avatar is done
+ * but could not retreive any result.
+ * You can pass a dummy avatar/image to this prop.
+ *
+ * If not passed, the component will return `null`
+ *
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ fallbackComponent?: JSX.Element;
+
+ /**
+ * Optional query options for `useQuery`
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component tries to resolve the icon of a given chain, then return an image.
+ * @returns an
with the src of the chain icon
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { ChainProvider, ChainIcon } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Result: An
component with the src of the icon
+ * ```html
+ *
+ * ```
+ *
+ * ### Override the icon with the `iconResolver` prop
+ * If you already have the icon url, you can skip the network requests and pass it directly to the ChainIcon
+ * ```tsx
+ *
+ * ```
+ *
+ * You can also pass in your own custom (async) function that retrieves the icon url
+ * ```tsx
+ * const getIcon = async () => {
+ * const icon = getIconFromCoinMarketCap(chainId, etc);
+ * return icon;
+ * };
+ *
+ *
+ * ```
+ *
+ * ### Show a loading sign while the icon is being loaded
+ * ```tsx
+ * } />
+ * ```
+ *
+ * ### Fallback to a dummy image if the chain icon fails to resolve
+ * ```tsx
+ * } />
+ * ```
+ *
+ * ### Usage with queryOptions
+ * ChainIcon uses useQuery() from tanstack query internally.
+ * It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @chain
+ * @beta
+ */
+export function ChainIcon({
+ iconResolver,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ client,
+ ...restProps
+}: ChainIconProps) {
+ const { chain } = useChainContext();
+ const iconQuery = useQuery({
+ queryKey: ["_internal_chain_icon_", chain.id] as const,
+ queryFn: async () => {
+ if (typeof iconResolver === "string") {
+ return iconResolver;
+ }
+ if (typeof iconResolver === "function") {
+ return iconResolver();
+ }
+ // Check if the chain object already has "icon"
+ if (chain.icon?.url) {
+ return chain.icon.url;
+ }
+ const possibleUrl = await getChainMetadata(chain).then(
+ (data) => data.icon?.url,
+ );
+ if (!possibleUrl) {
+ throw new Error("Failed to resolve icon for chain");
+ }
+ return resolveScheme({ uri: possibleUrl, client });
+ },
+ ...queryOptions,
+ });
+
+ if (iconQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!iconQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ return
;
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
new file mode 100644
index 00000000000..b8278e4e635
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.test.tsx
@@ -0,0 +1,73 @@
+import { describe, expect, it } from "vitest";
+import { render, screen, waitFor } from "~test/react-render.js";
+import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
+import { defineChain } from "../../../../../chains/utils.js";
+import { ChainName } from "./name.js";
+import { ChainProvider } from "./provider.js";
+
+describe.runIf(process.env.TW_SECRET_KEY)("ChainName component", () => {
+ it("should return the correct chain name, if the name exists in the chain object", () => {
+ render(
+
+
+ ,
+ );
+ waitFor(() =>
+ expect(
+ screen.getByText("Ethereum", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+
+ it("should return the correct chain name, if the name is loaded from the server", () => {
+ render(
+
+
+ ,
+ );
+ waitFor(() =>
+ expect(
+ screen.getByText("Ethereum Mainnet", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+
+ it("should return the correct FORMATTED chain name", () => {
+ render(
+
+ `${str}-formatted`} />
+ ,
+ );
+ waitFor(() =>
+ expect(
+ screen.getByText("Ethereum-formatted", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+
+ it("should fallback properly when fail to resolve chain name", () => {
+ render(
+
+ oops} />
+ ,
+ );
+
+ waitFor(() =>
+ expect(
+ screen.getByText("oops", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+});
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
new file mode 100644
index 00000000000..0bd4518128b
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/name.tsx
@@ -0,0 +1,185 @@
+"use client";
+
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type React from "react";
+import type { JSX } from "react";
+import { getChainMetadata } from "../../../../../chains/utils.js";
+import { useChainContext } from "./provider.js";
+
+/**
+ * Props for the ChainName component
+ * @component
+ * @chain
+ */
+export interface ChainNameProps
+ extends Omit, "children"> {
+ /**
+ * This prop can be a string or a (async) function that resolves to a string, representing the name of the chain
+ * This is particularly useful if you already have a way to fetch the chain name.
+ */
+ nameResolver?: string | (() => string) | (() => Promise);
+ /**
+ * A function to format the name's display value
+ * Particularly useful to avoid overflowing-UI issues
+ *
+ * ```tsx
+ * doSomething()} />
+ * ```
+ */
+ formatFn?: (str: string) => string;
+ /**
+ * This component will be shown while the name of the chain is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: JSX.Element;
+ /**
+ * This component will be shown if the name fails to be retreived
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a descriptive text/component to this prop, indicating that the
+ * name was not fetched succesfully
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+ fallbackComponent?: JSX.Element;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component fetches then shows the name of a chain.
+ * It inherits all the attributes of a HTML component, hence you can style it just like how you would style a normal
+ *
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { ChainProvider, ChainName } from "thirdweb/react";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * Ethereum Mainnet
+ * ```
+ *
+ * ### Custom name resolver
+ * By default ChainName will call the thirdweb API to retrieve the chain name.
+ * However if you have a different way to fetch the name, you can pass the function to the `nameResolver` prop.
+ * Note: nameResolver should either be a string or a function (async) that returns a string.
+ * ```tsx
+ * async function fetchNameMethod() {
+ * // your own fetching logic
+ * return "the chain name";
+ * }
+ *
+ *
+ * ```
+ *
+ * Alternatively you can also pass in a string directly:
+ * ```tsx
+ *
+ * ```
+ *
+ *
+ * ### Format the name (capitalize, truncate, etc.)
+ * The ChainName component accepts a `formatFn` which takes in a string and outputs a string
+ * The function is used to modify the name of the chain
+ *
+ * ```tsx
+ * const concatStr = (str: string):string => str + "Network"
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Result:
+ * ```html
+ * Ethereum Mainnet Network
+ * ```
+ *
+ * ### Show a loading sign when the name is being fetched
+ * ```tsx
+ * import { ChainProvider, ChainName } from "thirdweb/react";
+ *
+ *
+ * } />
+ *
+ * ```
+ *
+ * ### Fallback to something when the name fails to resolve
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @chain
+ * @beta
+ */
+export function ChainName({
+ nameResolver,
+ formatFn,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ ...restProps
+}: ChainNameProps) {
+ const { chain } = useChainContext();
+ const nameQuery = useQuery({
+ queryKey: ["_internal_chain_name_", chain.id] as const,
+ queryFn: async () => {
+ if (typeof nameResolver === "string") {
+ return nameResolver;
+ }
+ if (typeof nameResolver === "function") {
+ return nameResolver();
+ }
+ if (chain.name) {
+ return chain.name;
+ }
+ return getChainMetadata(chain).then((data) => data.name);
+ },
+ ...queryOptions,
+ });
+
+ if (nameQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!nameQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ const displayValue = formatFn ? formatFn(nameQuery.data) : nameQuery.data;
+
+ return {displayValue};
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.test.tsx
new file mode 100644
index 00000000000..cb50b4d22e0
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.test.tsx
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest";
+import { render, screen, waitFor } from "~test/react-render.js";
+import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
+import { ChainName } from "./name.js";
+import { ChainProvider } from "./provider.js";
+
+describe.runIf(process.env.TW_SECRET_KEY)("ChainProvider component", () => {
+ it("should render children correctly", () => {
+ render(
+
+ Child Component
+ ,
+ );
+
+ expect(screen.getByText("Child Component")).toBeInTheDocument();
+ });
+
+ it("should pass the chain correctly to the children props", () => {
+ render(
+
+
+ ,
+ );
+
+ waitFor(() =>
+ expect(
+ screen.getByText("Ethereum", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+});
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx
new file mode 100644
index 00000000000..97c21ca52d6
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Chain/provider.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import type React from "react";
+import { createContext, useContext } from "react";
+import type { Chain } from "../../../../../chains/types.js";
+
+/**
+ * Props for the component
+ * @component
+ * @chain
+ */
+export type ChainProviderProps = {
+ chain: Chain;
+};
+
+const ChainProviderContext = /* @__PURE__ */ createContext<
+ ChainProviderProps | undefined
+>(undefined);
+
+/**
+ * A React context provider component that supplies Chain-related data to its child components.
+ *
+ * This component serves as a wrapper around the `ChainProviderContext.Provider` and passes
+ * the provided chain data down to all of its child components through the context API.
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { ChainProvider, ChainIcon, ChainName } from "thirdweb/react";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ### Usage with defineChain
+ * ```tsx
+ * import { defineChain } from "thirdweb/chains"l
+ * import { ChainProvider, ChainName } from "thirdweb/react";
+ *
+ * const chainId = someNumber;
+ *
+ *
+ *
+ *
+ * ```
+ * @component
+ * @chain
+ */
+export function ChainProvider(
+ props: React.PropsWithChildren,
+) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+/**
+ * @internal
+ */
+export function useChainContext() {
+ const ctx = useContext(ChainProviderContext);
+ if (!ctx) {
+ throw new Error(
+ "ChainProviderContext not found. Make sure you are using ChainName, ChainIcon, etc. inside a component",
+ );
+ }
+ return ctx;
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
index 05076a5ceaa..4c9bb4cff6a 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/icon.tsx
@@ -7,6 +7,11 @@ import { getContractMetadata } from "../../../../../extensions/common/read/getCo
import { resolveScheme } from "../../../../../utils/ipfs.js";
import { useTokenContext } from "./provider.js";
+/**
+ * Props for the TokenIcon component
+ * @component
+ * @token
+ */
export interface TokenIconProps
extends Omit, "src"> {
/**
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/provider.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/provider.test.tsx
new file mode 100644
index 00000000000..bd7f0110ce3
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/provider.test.tsx
@@ -0,0 +1,44 @@
+import { describe, expect, it } from "vitest";
+import { render, screen, waitFor } from "~test/react-render.js";
+import { TEST_CLIENT } from "~test/test-clients.js";
+import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { TokenName } from "./name.js";
+import { TokenProvider } from "./provider.js";
+
+describe.runIf(process.env.TW_SECRET_KEY)("TokenProvider component", () => {
+ it("should render children correctly", () => {
+ render(
+
+ Child Component
+ ,
+ );
+
+ expect(screen.getByText("Child Component")).toBeInTheDocument();
+ });
+
+ it("should pass the token data correctly to the children props", () => {
+ render(
+
+
+ ,
+ );
+
+ waitFor(() =>
+ expect(
+ screen.getByText("Ether", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+});
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx
new file mode 100644
index 00000000000..0a09e414c18
--- /dev/null
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Token/symbol.test.tsx
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vitest";
+import { render, screen, waitFor } from "~test/react-render.js";
+import { TEST_CLIENT } from "~test/test-clients.js";
+import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { TokenProvider } from "./provider.js";
+import { TokenSymbol } from "./symbol.js";
+
+describe.runIf(process.env.TW_SECRET_KEY)("TokenSymbol component", () => {
+ it("should pass the address correctly to the children props", () => {
+ render(
+
+
+ ,
+ );
+
+ waitFor(() =>
+ expect(
+ screen.getByText("ETH", {
+ exact: true,
+ selector: "span",
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+});