diff --git a/app/frontend/src/components/PreviewBanner.tsx b/app/frontend/src/components/PreviewBanner.tsx
new file mode 100644
index 000000000..22db2db38
--- /dev/null
+++ b/app/frontend/src/components/PreviewBanner.tsx
@@ -0,0 +1,275 @@
+"use client";
+
+/**
+ * PreviewBanner
+ *
+ * Renders a sticky top banner that informs contributors which preview /
+ * testnet environment they are currently using.
+ *
+ * ┌─────────────────────────────────────────────────────────────────────────┐
+ * │ 🔬 PREVIEW ENVIRONMENT — staging │ branch: feat/FE-38 │ sha: a1b2c3d │
+ * └─────────────────────────────────────────────────────────────────────────┘
+ *
+ * Guarantees:
+ * • Never renders in production (isPreviewEnvironment() === false).
+ * • Degrades gracefully — if the backend is unavailable the banner still
+ * shows static environment metadata resolved from env vars.
+ * • Accessible: uses role="banner", aria-label, and a dismiss button with
+ * an aria-label.
+ * • Respects prefers-reduced-motion via a CSS media query on the shimmer.
+ */
+
+import { useState } from "react";
+import { usePreviewMetadata, isPreviewEnvironment } from "@/hooks/usePreviewMetadata";
+
+// ─── sub-components ──────────────────────────────────────────────────────────
+
+function Chip({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function Skeleton() {
+ return (
+
+ );
+}
+
+// ─── keyframes injected once via a