diff --git a/src/frontend/src/components/plugins/PluginPanel.tsx b/src/frontend/src/components/plugins/PluginPanel.tsx
new file mode 100644
index 0000000000..aef7959ed3
--- /dev/null
+++ b/src/frontend/src/components/plugins/PluginPanel.tsx
@@ -0,0 +1,93 @@
+import { t } from '@lingui/macro';
+import { Alert, Text } from '@mantine/core';
+import { AxiosInstance } from 'axios';
+import { useEffect, useRef } from 'react';
+
+import { api } from '../../App';
+import { ModelType } from '../../enums/ModelType';
+import { PanelType } from '../nav/Panel';
+
+interface PluginPanelProps extends PanelType {
+  src?: string;
+  params?: any;
+  targetModel?: ModelType | string;
+  targetId?: string | number | null;
+}
+
+/*
+ * Definition of what we pass into a plugin panel
+ */
+interface PluginPanelParameters {
+  target: HTMLDivElement;
+  props: PluginPanelProps;
+  targetModel?: ModelType | string;
+  targetId?: number;
+  api: AxiosInstance;
+}
+
+// Placeholder content for a panel with no content
+function PanelNoContent() {
+  return (
+    <Alert color="red" title={t`No Content`}>
+      <Text>{t`No content provided for this plugin`}</Text>
+    </Alert>
+  );
+}
+
+/**
+ * TODO: Provide more context information to the plugin renderer:
+ *
+ * - api instance
+ * - custom context data from server
+ */
+
+/**
+ * A custom panel which can be used to display plugin content.
+ *
+ * - Content is loaded dynamically (via the API) when a page is first loaded
+ * - Content can be provided from an external javascript module, or with raw HTML
+ *
+ * If content is provided from an external source, it is expected to define a function `render_panel` which will render the content.
+ * const render_panel = (element: HTMLElement, params: any) => {...}
+ *
+ * Where:
+ *  - `element` is the HTML element to render the content into
+ *  - `params` is the set of run-time parameters to pass to the content rendering function
+ */
+export default function PluginPanel({ props }: { props: PluginPanelProps }) {
+  const ref = useRef<HTMLDivElement>();
+
+  const loadExternalSource = async () => {
+    // Load content from external source
+    const src = await import(/* @vite-ignore */ props.src ?? '');
+
+    // We expect the external source to define a function which will render the content
+    if (src && src.render_panel && typeof src.render_panel === 'function') {
+      src.render_panel({
+        target: ref.current,
+        props: props,
+        api: api,
+        targetModel: props.targetModel,
+        targetId: props.targetId
+      });
+    }
+  };
+
+  useEffect(() => {
+    if (props.src) {
+      // Load content from external source
+      loadExternalSource();
+    } else if (props.content) {
+      // If content is provided directly, render it into the panel
+      // ref.current.innerHTML = props.content;
+    } else {
+      // Something... went wrong?
+    }
+  }, [props]);
+
+  if (!props.content && !props.src) {
+    return <PanelNoContent />;
+  }
+
+  return <div ref={ref as any}>{props.content}</div>;
+}
diff --git a/src/frontend/src/hooks/UsePluginPanels.tsx b/src/frontend/src/hooks/UsePluginPanels.tsx
index 52382bc995..b37d311e74 100644
--- a/src/frontend/src/hooks/UsePluginPanels.tsx
+++ b/src/frontend/src/hooks/UsePluginPanels.tsx
@@ -5,6 +5,7 @@ import { useMemo } from 'react';
 
 import { api } from '../App';
 import { PanelType } from '../components/nav/Panel';
+import PluginPanel from '../components/plugins/PluginPanel';
 import { ApiEndpoints } from '../enums/ApiEndpoints';
 import { ModelType } from '../enums/ModelType';
 import { identifierString } from '../functions/conversion';
@@ -16,15 +17,6 @@ export type PluginPanelState = {
   panels: PanelType[];
 };
 
-// Placeholder content for a panel with no content
-function PanelNoContent() {
-  return (
-    <Alert color="red" title={t`No Content`}>
-      <Text>{t`No content provided for this plugin`}</Text>
-    </Alert>
-  );
-}
-
 export function usePluginPanels({
   targetModel,
   targetId
@@ -39,6 +31,7 @@ export function usePluginPanels({
     [globalSettings]
   );
 
+  // API query to fetch information on available plugin panels
   const { isFetching, data } = useQuery({
     enabled: pluginPanelsEnabled && !!targetModel,
     queryKey: [targetModel, targetId],
@@ -70,7 +63,7 @@ export function usePluginPanels({
           name: identifierString(`${pluginKey}-${panel.name}`),
           label: panel.label || t`Plugin Panel`,
           icon: <InvenTreeIcon icon={panel.icon ?? 'plugin'} />,
-          content: panel.content || <PanelNoContent />
+          content: <PluginPanel props={panel} />
         };
       }) ?? []
     );