Skip to main content
This guide walks you through creating a Next.js application with Retab widgets from scratch. You’ll build a single-page demo that showcases all widgets in an interactive accordion.

Step 1: Create a New Next.js App

Create a new Next.js application using the App Router:
npm create-next-app@latest retab-app --typescript --tailwind --eslint --no-src-dir --app --no-react-compiler --import-alias "@/*" 
Install the required packages:
npm install @retab/react

Step 2: Environment Variables

Create a .env.local file in your project root:
.env
# Server-side only (never exposed to browser, available at https://retab.com/dashboard/settings)
RETAB_API_KEY=sk_retab_xxx

# Client-side (exposed to browser)
NEXT_PUBLIC_RETAB_PROJECT_ID=proj_xxx  # (available at https://retab.com/dashboard)
NEXT_PUBLIC_RETAB_BASE_URL=https://api.retab.com
Get your API key and project ID from the Retab dashboard.

Step 3: Create the Token Endpoint

app/api/retab/token/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  try {
    const retabApiKey = process.env.RETAB_API_KEY;

    if (!retabApiKey) {
      return NextResponse.json(
        { error: "RETAB_API_KEY is not set" },
        { status: 500 }
      );
    }

    const response = await fetch(
      `${process.env.NEXT_PUBLIC_RETAB_BASE_URL}/v1/auth/session`,
      {
        method: "POST",
        headers: {
          "Api-Key": retabApiKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ ttl: 3600 }),
      }
    );

    if (!response.ok) {
      return NextResponse.json(
        { error: "Failed to get session token" },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json({ token: data.token });
  } catch (error) {
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

Step 4: Create the Providers Component

Since the root layout is a Server Component and RetabProvider requires client-side features, create a separate client component for providers.
app/providers.tsx
"use client";

import { ReactNode } from "react";
import { RetabProvider } from "@retab/react";

interface ProvidersProps {
  children: ReactNode;
}

export function Providers({ children }: ProvidersProps) {
  return (
    <RetabProvider
      projectId={process.env.NEXT_PUBLIC_RETAB_PROJECT_ID!}
      authConfig={{
        getToken: async () => {
          const response = await fetch("/api/retab/token");
          if (!response.ok) {
            throw new Error("Failed to get Retab token");
          }
          const { token } = await response.json();
          return token;
        },
        baseUrl: process.env.NEXT_PUBLIC_RETAB_BASE_URL || "https://api.retab.com",
      }}
    >
      {children}
    </RetabProvider>
  );
}

Step 5: Configure the Root Layout

app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
import "@retab/react/styles.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Retab Widgets Demo",
  description: "Interactive demo of Retab React widgets",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}
The root layout remains a Server Component (no “use client”), which allows you to use metadata exports and other server-only features. The Providers component handles the client-side context.

Step 6: Create the Widgets Demo Page

app/page.tsx
"use client";

import { useState, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
  useExtractions,
  DataComponent,
  FileComponent,
  ExtractionsList,
  ExtractionReviewer,
  ExtractionComponent,
  UploadJobsList,
} from "@retab/react";

type WidgetId = "data" | "file" | "list" | "reviewer" | "extraction" | "uploads";

interface Widget {
  id: WidgetId;
  name: string;
  component: string;
  description: string;
  category: "display" | "composite" | "list";
}

const widgets: Widget[] = [
  {
    id: "data",
    name: "DataComponent",
    component: "<DataComponent />",
    description: "Display and edit extracted data in form, table, or code view",
    category: "display",
  },
  {
    id: "file",
    name: "FileComponent",
    component: "<FileComponent />",
    description: "Preview documents with PDF rendering and field highlighting",
    category: "display",
  },
  {
    id: "list",
    name: "ExtractionsList",
    component: "<ExtractionsList />",
    description: "Browse extractions with search, filters, and pagination",
    category: "list",
  },
  {
    id: "reviewer",
    name: "ExtractionReviewer",
    component: "<ExtractionReviewer />",
    description: "Complete review interface with list, file preview, and data editor",
    category: "composite",
  },
  {
    id: "extraction",
    name: "ExtractionComponent",
    component: "<ExtractionComponent />",
    description: "Side-by-side file preview and data display with resizable panels",
    category: "composite",
  },
  {
    id: "uploads",
    name: "UploadJobsList",
    component: "<UploadJobsList />",
    description: "Upload files and track processing jobs",
    category: "list",
  },
];

const categories = [
  { id: "display", label: "Display Components" },
  { id: "composite", label: "Composite Components" },
  { id: "list", label: "List Components" },
] as const;

function WidgetRenderer({ widgetId }: { widgetId: WidgetId }) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const projectId = process.env.NEXT_PUBLIC_RETAB_PROJECT_ID!;

  const { extractions } = useExtractions();
  const firstExtraction = extractions[0] ?? null;
  const extractionId = searchParams.get("extractionId") || firstExtraction?.id;

  const handleNavigate = useCallback(
    (params: { extractionId?: string }) => {
      const url = params.extractionId
        ? `/?widget=${widgetId}&extractionId=${params.extractionId}`
        : `/?widget=${widgetId}`;
      router.push(url);
    },
    [router, widgetId]
  );

  switch (widgetId) {
    case "data":
      return (
        <DataComponent
          extraction={firstExtraction}
          extractionDisplayOptions={{
            view: "form",
            showTabs: true,
            allowEditing: true,
          }}
        />
      );

    case "file":
      return (
        <FileComponent
          extraction={firstExtraction}
          fieldPath={null}
        />
      );

    case "list":
      return (
        <div className="flex flex-1 flex-col min-h-0 p-2">
          <ExtractionsList
            visibility={{
              statusColumn: true,
              dateColumn: true,
              search: true,
              displayPopover: true,
              filters: true,
            }}
          />
        </div>
      );

    case "reviewer":
      return (
        <ExtractionReviewer
          projectId={projectId}
          extractionId={extractionId}
          onNavigate={handleNavigate}
          visibility={{
            search: true,
            uploadButton: true,
            extractionDisplayOptions: {
              allowEditing: true,
              showTabs: true,
            },
          }}
        />
      );

    case "extraction":
      return (
        <ExtractionComponent
          extractionId={firstExtraction?.id}
          projectId={projectId}
          extractionDisplayOptions={{
            view: "form",
            allowEditing: true,
            showTabs: true,
          }}
        />
      );

    case "uploads":
      return <div className="flex flex-1 flex-col min-h-0 p-2"><UploadJobsList /></div>;

    default:
      return null;
  }
}

export default function WidgetsDemo() {
  const [selectedWidget, setSelectedWidget] = useState<WidgetId>("reviewer");
  const [expandedCategories, setExpandedCategories] = useState<string[]>([
    "display",
    "composite",
    "list",
  ]);

  const toggleCategory = (categoryId: string) => {
    setExpandedCategories((prev) =>
      prev.includes(categoryId)
        ? prev.filter((id) => id !== categoryId)
        : [...prev, categoryId]
    );
  };

  const currentWidget = widgets.find((w) => w.id === selectedWidget)!;

  return (
    <div className="h-screen flex">
      {/* Sidebar */}
      <div className="w-72 border-r bg-gray-50 flex flex-col">
        <div className="p-4 border-b">
          <h1 className="text-lg font-semibold">Retab Widgets</h1>
          <p className="text-sm text-gray-500 mt-1">
            Interactive component demo
          </p>
        </div>

        <div className="flex-1 overflow-auto p-2">
          {categories.map((category) => {
            const categoryWidgets = widgets.filter(
              (w) => w.category === category.id
            );
            const isExpanded = expandedCategories.includes(category.id);

            return (
              <div key={category.id} className="mb-2">
                <button
                  onClick={() => toggleCategory(category.id)}
                  className="w-full flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded"
                >
                  <span>{category.label}</span>
                  <svg
                    className={`w-4 h-4 transition-transform ${isExpanded ? "rotate-180" : ""
                      }`}
                    fill="none"
                    stroke="currentColor"
                    viewBox="0 0 24 24"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      strokeWidth={2}
                      d="M19 9l-7 7-7-7"
                    />
                  </svg>
                </button>

                {isExpanded && (
                  <div className="mt-1 ml-2">
                    {categoryWidgets.map((widget) => (
                      <button
                        key={widget.id}
                        onClick={() => setSelectedWidget(widget.id)}
                        className={`w-full text-left px-3 py-2 text-sm rounded transition-colors ${selectedWidget === widget.id
                          ? "bg-indigo-100 text-indigo-700"
                          : "text-gray-600 hover:bg-gray-100"
                          }`}
                      >
                        <code className="text-xs">{widget.component}</code>
                      </button>
                    ))}
                  </div>
                )}
              </div>
            );
          })}
        </div>

        <div className="p-4 border-t bg-white">
          <p className="text-xs text-gray-500">
            Import from{" "}
            <code className="text-indigo-600 bg-indigo-50 px-1 rounded">
              @retab/react
            </code>
          </p>
        </div>
      </div>

      {/* Main Content */}
      <div className="flex-1 flex flex-col min-w-0">
        {/* Header */}
        <div className="border-b px-4 py-3 bg-white">
          <div className="flex items-center justify-between">
            <code className="text-indigo-600 font-mono text-sm">
              {currentWidget.component}
            </code>
            <p className="text-sm text-gray-500 mt-1">
              {currentWidget.description}
            </p>
          </div>
        </div>

        {/* Widget Preview */}
        <div className="flex-1 min-h-0 flex">
          <WidgetRenderer widgetId={selectedWidget} />
        </div>
      </div>
    </div>
  );
}

Step 7: Run the Application

Start your development server:
npm run dev
Open http://localhost:3000 to see the widgets demo. You can:
  • Browse widgets by category in the sidebar accordion
  • Click on any widget to see it live
  • Interact with the widgets using your project’s data

Project Structure

Project Structure
retab-app/
├── app/
│   ├── api/
│   │   └── retab/
│   │       └── token/
│   │           └── route.ts
│   ├── providers.tsx      # Client-side providers
│   ├── layout.tsx         # Server-side root layout
│   ├── page.tsx
│   └── globals.css
├── .env
└── package.json

Adding Authentication

In production, protect the token endpoint with your auth solution:
app/api/retab/token/route.ts
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await getServerSession();
  
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  
  // ... rest of token generation
}

Next Steps

  • Add your preferred authentication
  • Customize the sidebar styling
  • Deploy to Vercel or your hosting platform