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

# TableElement

> An advertising account's campaigns, ad groups and ads in one table, with the tabs that move between them: pick rows to narrow the level below, search, sort, break the numbers down, choose your columns, and pause, resume, duplicate or delete straight from a row. The window it reports on and the rows it is filtered to live on the handle, so a chart mounted beside it reports on exactly the same thing.

<Info>**Upcoming** — generated from the latest merged element source; documents unreleased development. Use the channel picker at the top of the sidebar for the docs of a published release.</Info>

*In development — not yet part of a stable release.*

Belongs to the [`Reporting`](/elements/upcoming/ads/reporting) sub-controller in the [`Ads`](/elements/upcoming/ads/overview) group. Render `<TableElement />` inside `<Reporting>` (React), or mint the sub and mount off it (vanilla): `ads.create('reporting', { … }).create('table', { … })` — consumer props and `on<Event>` callbacks both go in the create options / JSX props.

## Preview

A live, interactive demo of this element with sample data:

<div data-whop-demo-shell style={{ position: "relative", minHeight: "320px", transition: "min-height 200ms ease" }}>
  <div data-whop-demo-skeleton style={{ position: "absolute", inset: "0", borderRadius: "12px", background: "rgba(140, 140, 140, 0.12)", pointerEvents: "none", transition: "opacity 200ms ease" }} />

  <div data-whop-demo-native="element:reporting/table" data-whop-elements-version="" style={{ position: "relative" }} />
</div>

## Usage

<div data-whop-usage="ads/reporting-table">
  <CodeGroup>
    ```tsx React theme={null}
    import { WhopElements, Ads, Reporting, TableElement } from "@whop/elements-react";
    import { loadWhop } from "@whop/elements";

    function Example() {
      return (
        <WhopElements elements={loadWhop()}>
          <Ads /* options */>
            <Reporting>
              <TableElement onTabChanged={(payload) => console.log("tabChanged", payload)} onFilterChanged={(payload) => console.log("filterChanged", payload)} onColumnsChanged={(payload) => console.log("columnsChanged", payload)} onCustomMetricsChanged={(payload) => console.log("customMetricsChanged", payload)} onViewChanged={(payload) => console.log("viewChanged", payload)} onSourcesChanged={(payload) => console.log("sourcesChanged", payload)} onAttributionModelChanged={(payload) => console.log("attributionModelChanged", payload)} />
            </Reporting>
          </Ads>
        </WhopElements>
      );
    }
    ```

    ```html Vanilla theme={null}
    <script src="https://js.whop.cloud/elements/amber/elements.js" data-whop-elements></script>
    <script type="module">
      const ads = window.WhopElements().ads.create({ /* options */ });
      const reporting = ads.create('reporting', { /* options */ });
      reporting.create('table', {
        onTabChanged: (payload) => console.log("tabChanged", payload),
        onFilterChanged: (payload) => console.log("filterChanged", payload),
        onColumnsChanged: (payload) => console.log("columnsChanged", payload),
        onCustomMetricsChanged: (payload) => console.log("customMetricsChanged", payload),
        onViewChanged: (payload) => console.log("viewChanged", payload),
        onSourcesChanged: (payload) => console.log("sourcesChanged", payload),
        onAttributionModelChanged: (payload) => console.log("attributionModelChanged", payload)
      }).mount('#ads-reporting-table');
    </script>
    ```
  </CodeGroup>
</div>

## Props

<ResponseField name="accessToken" type="string">
  A scoped token. Reading needs `ad_campaign:read`, `ad_group:read`, `ad:read` and `stats:read`; the row actions additionally need the matching `:update` and `:delete` scopes, and duplicating needs `:create`. Mint it on your server with `POST /v1/access_tokens` and set a fresh one before it expires. Omitted, the calls carry the viewer's own session, which only answers same-origin. To have the table pick up the account's own reporting timezone and currency it additionally needs `ad_campaign:create`, which is what the preferences endpoint asks for; without it the figures are reported in the viewer's zone unless you set `timezone`/`currency` on the handle.
</ResponseField>

<ResponseField name="useCampaignCreator" type="boolean">
  What happens when a viewer asks to edit — from a row menu, a draft row, or Edit ad in the details overlay. On by default: the campaign builder opens over the page, on the campaign and step they asked for, so editing works with nothing wired up. Turn it off to handle campaign editing yourself — nothing opens, and the request comes back on `editRequested`. Defaults to `true`.
</ResponseField>

<ResponseField name="breakdown" type="&#x22;age&#x22; | &#x22;gender&#x22; | &#x22;age_gender&#x22; | &#x22;placement&#x22; | &#x22;publisher_platform&#x22; | &#x22;device_platform&#x22; | &#x22;impression_device&#x22; | &#x22;country&#x22; | &#x22;region&#x22; | &#x22;hour&#x22; | null">
  How rows are broken down — by country, platform, placement — or `null` for whole rows. Defaults to `null`.
</ResponseField>

<ResponseField name="columns" type="string[] | null">
  The columns on show, by name — standard ones like `impressions` and `roas`, and any pixel events the viewer added. Unset, the table opens on its own defaults. Comes back whole on `columnsChanged`; store the list and pass it here to have the layout stick. Defaults to `null`.
</ResponseField>

<ResponseField name="customMetrics" type="CustomMetricDefinition[]">
  Columns computed from the figures already in the table — a formula over them, named and formatted by you. The table can edit them, and every change comes back on `customMetricsChanged` as the whole new list; persist it and pass it here to have it stick. Defaults to `[]`.
</ResponseField>

<ResponseField name="sourcesDrill" type="string | null">
  What the All tab is drilled into, as a source path: `null` for the roots, `ext:*` for a platform list, `ext:<platform>:*` for that platform’s campaigns. Comes back on `viewChanged`. Defaults to `null`.
</ResponseField>

<ResponseField name="query" type="string">
  What the Ads tab’s search box is filtered to. Defaults to `""`.
</ResponseField>

<ResponseField name="showPausedCampaigns" type="boolean">
  Whether paused campaigns are listed. Defaults to `true`.
</ResponseField>

### Direct mode only

*These props exist only on the in-process (direct-mode) component — the hosted embed never carries them, and the element renders without them.*

<ResponseField name="inheritLayout" type="direct mode only">
  Direct mode only. Leave the viewer’s columns and computed metrics to the app around the table — its own store, its own defaults — instead of taking them from `columns` and `customMetrics`. An embed cannot set this and should not want to: localStorage inside a frame belongs to the bundle’s origin, so a layout chosen there is invisible to everything else you run.
</ResponseField>

<ResponseField name="navigation" type="direct mode only">
  Direct mode only. Hand the table your router and its links become real navigations — a `Link` for the row menus, a `navigate` that honours cmd-click. Left unset, and in every embed, links raise `editRequested` instead: an element frame has no router, and a plain `<a>` would move the frame rather than the page around it.
</ResponseField>

## Events

Pass a callback in the create options (or React prop) to receive these.

### `onTabChanged`

The open tab changed. Mirror it into your own URL if you want it to survive a reload.

**Signature:** `((payload: { tab: "ads" | "all" | "campaigns" | "ad-groups"; }) => void)`

### `onFilterChanged`

The viewer narrowed the view — by ticking rows, or by clicking through a campaign to its ad groups. Every surface under the same handle has already followed it.

**Signature:** `((payload: { campaignIds: string[]; adGroupIds: string[]; adIds: string[]; }) => void)`

### `onColumnsChanged`

The viewer showed or hid a column. Carries every column now on show, standard and pixel-event alike — store the list and pass it straight back as `columns`, or the layout resets on the next mount.

**Signature:** `((payload: { columns: string[]; }) => void)`

### `onCustomMetricsChanged`

The viewer added, edited or removed a computed column. Carries the WHOLE list either way — store it as given and pass it back as `customMetrics`, or they are gone on the next mount.

**Signature:** `((payload: { customMetrics: CustomMetricDefinition[]; }) => void)`

### `onViewChanged`

The viewer moved one of the table’s own controls — the All tab’s drill, the breakdown, the search box, the paused toggle. Carries all four whichever moved; store them and pass them back as props, or they reset on the next mount.

**Signature:** `((payload: { sourcesDrill: string | null; breakdown: "age" | "gender" | "age_gender" | "placement" | "publisher_platform" | "device_platform" | "impression_device" | "country" | "region" | "hour" | null; query: string; showPausedCampaigns: boolean; }) => void)`

### `onSourcesChanged`

What the All tab is now looking at, as source paths ready to hand to a chart: `null` off the All tab (Whop advertising alone), `[]` for every source, otherwise the ticked rows or the drilled-into group. Every surface under the same handle has already followed it.

**Signature:** `((payload: { sources: string[] | null; }) => void)`

### `onAttributionModelChanged`

The viewer changed which touch gets the credit, from the picker on the All tab. Every surface under the same handle has already re-credited; persist it and pass it back as `attributionModel`, or it reverts to last touch on the next mount.

**Signature:** `((payload: { attributionModel: "last_touch" | "first_touch"; }) => void)`

### `onLoaderStart`

Fired the moment the element's own loading skeleton has painted inside its frame — the earliest point a consumer-managed loading state can hand off without ever exposing a blank. Always precedes `onReady`; most consumers only need `onReady`.

**Signature:** `(() => void)`

### `onReady`

Fired once the element has booted and painted its first complete frame.

**Signature:** `(() => void)`

### `onError`

Fired when the element fails to load or crashes; the element shows its own error fallback. `message` is human-readable; framework refusals also carry `code` (e.g. `HOST_SOURCE_FAILED`, with `sourceKey` naming the failed hostState key) so hosts can switch on codes, never message text.

**Signature:** `((e: { message: string; code?: string | undefined; sourceKey?: string | undefined; }) => void)`

## Methods

Call these on the element handle — the return of `create` (vanilla) or the component `ref` (React).

### `mount`

Place the element on the page: appends its container to `target` (a CSS selector or an element) and starts loading. Nothing renders until this is called. React consumers never call it — the component mounts itself.

**Signature:** `(target: string | HTMLElement) => void`

### `destroy`

Remove the element from the page and release its frame and subscriptions. Safe to call more than once. React consumers never call it — unmounting the component does it.

**Signature:** `() => void`

### `update`

Change this element's consumer props after mount — it re-renders with the merged props. React consumers never call it — updating the JSX props does the same.

**Signature:** `(options: Partial<TableElementProps>) => void`

## Styling

This element exposes no targetable class names. Use `appearance` (theme, accent color, variables) to restyle it.
