> ## Documentation Index
> Fetch the complete documentation index at: https://docs.retab.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js Quickstart

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:

<CodeGroup>
  ```bash npm theme={null}
  npm create-next-app@latest retab-app --typescript --tailwind --eslint --no-src-dir --app --no-react-compiler --import-alias "@/*" 
  ```

  ```bash pnpm theme={null}
  pnpm create-next-app@latest retab-app --typescript --tailwind --eslint --no-src-dir --app --no-react-compiler --import-alias "@/*"
  ```

  ```bash yarn theme={null}
  yarn create-next-app@latest retab-app --typescript --tailwind --eslint --no-src-dir --app --no-react-compiler --import-alias "@/*"
  ```

  ```bash bun theme={null}
  bun create-next-app@latest retab-app --typescript --tailwind --eslint --no-src-dir --app --no-react-compiler --import-alias "@/*"
  ```
</CodeGroup>

Install the required packages:

<CodeGroup>
  ```bash npm theme={null}
  npm install @retab/react
  ```

  ```bash pnpm theme={null}
  pnpm add @retab/react
  ```

  ```bash yarn theme={null}
  yarn add @retab/react
  ```

  ```bash bun theme={null}
  bun add @retab/react
  ```
</CodeGroup>

## Step 2: Environment Variables

Create a `.env.local` file in your project root:

```env .env theme={null}
# 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
```

<Tip>
  Get your API key and project ID from the [Retab
  dashboard](https://retab.com/dashboard/settings).
</Tip>

## Step 3: Create the Token Endpoint

```typescript app/api/retab/token/route.ts theme={null}
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: {
          "Authorization": `Bearer ${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.

```tsx app/providers.tsx theme={null}
"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

```tsx app/layout.tsx theme={null}
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>
  );
}
```

<Note>
  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.
</Note>

## Step 6: Create the Widgets Demo Page

```tsx app/page.tsx theme={null}
"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:

<CodeGroup>
  ```bash npm theme={null}
  npm run dev
  ```

  ```bash pnpm theme={null}
  pnpm dev
  ```

  ```bash yarn theme={null}
  yarn dev
  ```

  ```bash bun theme={null}
  bun dev
  ```
</CodeGroup>

Open [http://localhost:3000](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

```text Project Structure theme={null}
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:

```typescript app/api/retab/token/route.ts theme={null}
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
