diff --git a/.changeset/typography-text-node.md b/.changeset/typography-text-node.md
new file mode 100644
index 0000000000..7b5e9519ba
--- /dev/null
+++ b/.changeset/typography-text-node.md
@@ -0,0 +1,22 @@
+---
+'@leafygreen-ui/typography': minor
+---
+
+Adds `TextNode` component.
+
+Wraps a string in the provided `as` component,
+or renders the provided `ReactNode`.
+
+Useful when rendering `children` props that can be any react node
+
+```tsx
+Hello!
+// Renders:
Hello!
+```
+
+```tsx
+
+ Hello!
+
+// Renders: Hello!
+```
\ No newline at end of file
diff --git a/packages/typography/src/TextNode/TestNode.spec.tsx b/packages/typography/src/TextNode/TestNode.spec.tsx
new file mode 100644
index 0000000000..057b5f7b25
--- /dev/null
+++ b/packages/typography/src/TextNode/TestNode.spec.tsx
@@ -0,0 +1,128 @@
+import React, { PropsWithChildren } from 'react';
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+
+import { TextNode } from './TextNode';
+
+describe('packages/typography/TextNode', () => {
+ describe('when children is a string', () => {
+ test('renders string children wrapped in Polymorph component', () => {
+ render(Test string content);
+ expect(screen.getByText('Test string content')).toBeInTheDocument();
+ });
+
+ test('renders as a div by default', () => {
+ render(Test string content);
+ expect(
+ screen.getByText('Test string content').tagName.toLowerCase(),
+ ).toEqual('div');
+ });
+
+ test('renders with HTML element', () => {
+ const { container } = render(
+ Test paragraph content,
+ );
+ const paragraph = container.querySelector('p');
+ expect(paragraph).toBeInTheDocument();
+ expect(paragraph).toHaveTextContent('Test paragraph content');
+ });
+
+ test('renders as React component', () => {
+ const Wrapper = ({ children }: PropsWithChildren<{}>) => (
+ {children}
+ );
+ const { container } = render(
+ Test paragraph content,
+ );
+ const wrapperEl = screen.getByTestId('wrapper');
+ expect(wrapperEl).toBeInTheDocument();
+ });
+ });
+
+ describe('when children is a React node', () => {
+ test('renders React node children directly without wrapping', () => {
+ const testContent = (
+
+ Nested content
+
+ );
+
+ render({testContent});
+
+ expect(screen.getByTestId('test-div')).toBeInTheDocument();
+ expect(screen.getByText('Nested content')).toBeInTheDocument();
+ });
+
+ test('renders multiple React node children', () => {
+ render(
+
+ First
+ Second
+ ,
+ );
+
+ expect(screen.getByTestId('first-span')).toBeInTheDocument();
+ expect(screen.getByTestId('second-span')).toBeInTheDocument();
+ });
+
+ test('ignores as prop when children is not a string', () => {
+ const { container } = render(
+
+ React node content
+ ,
+ );
+
+ // Should not create a paragraph wrapper
+ expect(container.querySelector('p')).not.toBeInTheDocument();
+ // Should render the div directly
+ expect(screen.getByTestId('test-div')).toBeInTheDocument();
+ });
+
+ test('renders complex nested React components', () => {
+ const ComplexComponent = () => (
+
+
Complex Title
+
Complex paragraph
+
+ );
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText('Complex Title')).toBeInTheDocument();
+ expect(screen.getByText('Complex paragraph')).toBeInTheDocument();
+ });
+ });
+
+ describe('edge cases', () => {
+ test('handles empty children', () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ test('handles null children', () => {
+ const { container } = render({null});
+ expect(container.firstChild).toBeNull();
+ });
+
+ test('handles undefined children', () => {
+ const { container } = render({undefined});
+ expect(container.firstChild).toBeNull();
+ });
+
+ test('handles number children as string', () => {
+ render({42});
+ expect(screen.getByText('42')).toBeInTheDocument();
+ expect(screen.getByText('42').tagName.toLowerCase()).toEqual('span');
+ });
+
+ test('handles boolean children', () => {
+ const { container } = render({true});
+ // React doesn't render boolean values
+ expect(container.firstChild).toBeNull();
+ });
+ });
+});
diff --git a/packages/typography/src/TextNode/TextNode.tsx b/packages/typography/src/TextNode/TextNode.tsx
new file mode 100644
index 0000000000..b2ecc46a2a
--- /dev/null
+++ b/packages/typography/src/TextNode/TextNode.tsx
@@ -0,0 +1,29 @@
+import React, { PropsWithChildren } from 'react';
+import { Polymorph, type PolymorphicAs } from '@leafygreen-ui/polymorphic';
+
+/**
+ * Wraps a string in the provided `as` component,
+ * or renders the provided `ReactNode`.
+ *
+ * Useful when rendering `children` props that can be any react node
+ *
+ * @example
+ * ```
+ * Hello! // Hello!
+ * ```
+ *
+ * @example
+ * ```
+ * Hello!
// Hello!
+ * ```
+ */
+export const TextNode = ({
+ children,
+ as,
+}: PropsWithChildren<{ as?: PolymorphicAs }>) => {
+ return typeof children === 'string' || typeof children === 'number' ? (
+ {children}
+ ) : (
+ children
+ );
+};
diff --git a/packages/typography/src/index.ts b/packages/typography/src/index.ts
index 048b16f53a..dbd06def7c 100644
--- a/packages/typography/src/index.ts
+++ b/packages/typography/src/index.ts
@@ -33,6 +33,7 @@ export type { OverlineProps } from './Overline/Overline.types';
export { bodyTypeScaleStyles } from './styles';
export { default as Subtitle } from './Subtitle/Subtitle';
export type { SubtitleProps } from './Subtitle/Subtitle.types';
+export { TextNode } from './TextNode/TextNode';
export { DEFAULT_LGID_ROOT, getLgIds, type GetLgIdsReturnType } from './utils';
export { StaticWidthText } from './utils/StaticWidthText';
export { useUpdatedBaseFontSize } from './utils/useUpdatedBaseFontSize';