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

# Server-rendered ads

> React components that render Gravity ads from a server-delivered spec — ship new ad designs with no publisher code change or SDK upgrade.

## Installation

```bash theme={null}
npm install @gravity-ai/ui
```

Set `GRAVITY_API_KEY` in your server environment.

## How the pieces fit together

<Steps>
  <Step title="Publisher — drop in the component">
    Place `<GravityAd />` where the ad should appear. For a ShadCN/Tailwind app that's the entire integration.
  </Step>

  <Step title="Client — measure and fetch">
    On mount, `GravityAd` measures its container with a `ResizeObserver`, sends the dimensions to the engine, and fetches the ad for the placement.
  </Step>

  <Step title="Gravity — return ad + renderer">
    The engine returns the ad object with a `renderer_spec`: a JSON layout tree describing the full unit. We can change that spec server-side at any time.
  </Step>

  <Step title="Client — render">
    The SDK's runtime interprets the spec into DOM, fires the impression pixel on visibility, and routes clicks through the tracked URL.
  </Step>
</Steps>

Because the layout lives in the response, Gravity ships a new ad design from the server — no publisher code change, no SDK upgrade.

## Render ads

```tsx theme={null}
import { GravityAd } from '@gravity-ai/ui';

function ChatResponse({ response }) {
  return (
    <div>
      <p>{response}</p>
      <GravityAd placement="below_response" apiKey={process.env.GRAVITY_API_KEY} />
    </div>
  );
}
```

| Prop                             | Type             | Description                                                                                  |
| -------------------------------- | ---------------- | -------------------------------------------------------------------------------------------- |
| `placement`                      | `string`         | Placement identifier (required).                                                             |
| `apiKey`                         | `string`         | Publisher API key. Defaults to `GRAVITY_API_KEY`.                                            |
| `ad`                             | `GravityAdData`  | Pass ad data directly instead of fetching.                                                   |
| `variant`                        | `GravityVariant` | Hard-pin a bundled layout, overriding the server spec. Omit to let Gravity drive the render. |
| `query`                          | `string`         | User query/context for contextual serving.                                                   |
| `sessionId`                      | `string`         | Stable session ID so a user sees a consistent render within a session.                       |
| `showLabel`                      | `boolean`        | Show the "Ad"/sponsored label.                                                               |
| `onLoad` / `onEmpty` / `onClick` | callbacks        | Lifecycle hooks. `onLoad` includes measured dimensions.                                      |

The component handles impression tracking automatically via `IntersectionObserver`.

## Theme

ShadCN/Tailwind apps need nothing — the ad reads their existing CSS variables and follows light/dark mode. Other apps wrap once with `GravityThemeProvider`:

```tsx theme={null}
import { GravityThemeProvider } from '@gravity-ai/ui';

<GravityThemeProvider theme={{ primary: '330 81% 60%', radius: '0.75rem' }}>
  <App />
</GravityThemeProvider>
```

## Renderer spec

The ad response carries a `renderer_spec` — the JSON tree the SDK renders. Render precedence:

1. Publisher `variant` prop — a hard pin. Always wins.
2. Server `renderer_spec` — the normal path. Gravity ships the look remotely.
3. Bundled fallback — the SDK's built-in, dimension-aware look. Used only when neither above is present, so the unit always renders something sensible with zero config.

Whether an ad response carries a `renderer_spec` at all is decided server-side, per placement: Gravity attaches the spec when the placement has a default design configured on our end. There is no request parameter to enable it — see [how to get one set up](#want-a-custom-default-look-for-your-placement).

A spec for a single line of text:

```json theme={null}
{
  "version": 1,
  "name": "text-line",
  "root": {
    "type": "link",
    "style": { "display": "inline", "fontSize": "14px", "color": "hsl(var(--muted-foreground))" },
    "children": [
      { "type": "text", "text": "↗ ", "style": { "color": "hsl(var(--primary))", "fontWeight": 600 } },
      { "type": "text", "bind": "title", "style": { "color": "hsl(var(--foreground))", "fontWeight": 500 } },
      { "type": "text", "bind": "cta", "style": { "color": "hsl(var(--primary))", "fontWeight": 600 } }
    ]
  }
}
```

### Node types

| `type`  | Renders as | Notes                                                        |
| ------- | ---------- | ------------------------------------------------------------ |
| `box`   | `<div>`    | Layout container. Use `style` for flex/grid/positioning.     |
| `text`  | `<span>`   | Content from `bind`, static `text`, or a `{field}` template. |
| `image` | `<img>`    | `src` from `bind` or a static `src`. URL-validated.          |
| `link`  | `<a>`      | Wraps `children`. Defaults to the ad's tracked click URL.    |

Every node also supports:

* `bind` — pulls a value from the ad payload. Allowed: `adText`, `title`, `cta`, `brandName`, `url`, `favicon`.
* `text` — static text with `{field}` placeholders (e.g. `"Visit {brandName}"`).
* `showIf` — render this node only when the given ad field is non-empty.
* `style` — inline CSS (see below).

## Test a spec locally

Pass the `ad` prop and `GravityAd` skips the network fetch and renders exactly what you give it — including any `renderer_spec`. Use this to preview a unit before it goes live or to integration-test without hitting the engine.

```tsx theme={null}
import { GravityAd } from '@gravity-ai/ui';

const previewAd = {
  title: 'Build AI agents faster',
  cta: 'Try it free',
  brandName: 'Vercel',
  url: 'https://vercel.com',
  adText: 'Vercel AI SDK',
  renderer_spec: {
    version: 1,
    name: 'text-line',
    root: {
      type: 'link',
      style: { display: 'inline', fontSize: '14px' },
      children: [
        { type: 'text', bind: 'title', style: { color: 'hsl(var(--foreground))', fontWeight: 500 } },
        { type: 'text', bind: 'cta', style: { color: 'hsl(var(--primary))', fontWeight: 600 } },
      ],
    },
  },
};

<GravityAd placement="below_response" ad={previewAd} />
```

Swap the `renderer_spec` to see any layout render against your live theme — same rendering path as production. Leave `impUrl`/`clickUrl` off the mock ad and no impression or click tracking fires, so it's a pure visual preview.

<Tip>
  `renderer_spec` accepts the spec object or a JSON string, so you can paste a spec straight from a file or API response.
</Tip>

## Styling

The `style` object is plain CSS with camelCase keys. Two ways to style, mix freely.

**Reference theme tokens** for a native look that follows the publisher's light/dark mode:

```json theme={null}
{
  "type": "box",
  "style": {
    "background": "hsl(var(--card))",
    "color": "hsl(var(--card-foreground))",
    "border": "1px solid hsl(var(--border))",
    "borderRadius": "var(--radius)",
    "padding": "16px"
  }
}
```

Common tokens: `--background`, `--foreground`, `--card`, `--primary`, `--muted-foreground`, `--accent`, `--border`, `--radius`.

**Hardcode values** for pixel-perfect control. Any CSS property works:

```json theme={null}
{
  "type": "text",
  "bind": "brandName",
  "style": { "display": "block", "fontSize": "56px", "fontWeight": 800, "textTransform": "uppercase", "color": "#ff2d92" }
}
```

<Tip>
  Lay the unit out with theme tokens so it feels native, then hardcode the one or two properties (a brand color, a font size) where you want exact control.
</Tip>

## Best practices

A few rules keep server-rendered ads native-feeling and safe to evolve.

### The spec owns the whole unit; data binds per-ad

A `renderer_spec` describes the **entire ad container**, not a patch over your own markup — the runtime renders the full tree from `root` down. What stays dynamic is the *content*: `bind` a `text`/`image` node to an ad field and the same spec re-renders correctly for every different ad the engine serves. So you design the look once; the words, images, and links come from the live ad. Use `showIf` so optional parts (e.g. a brand image row) disappear gracefully when a field is empty.

### Lay it out with tokens, hardcode only what must be locked

Write layout, color, radius, and type as **theme tokens** (`hsl(var(--primary))`, `var(--radius)`). The ad then inherits the host app's styling at paint time — when a publisher restyles their site or flips dark mode, the ad follows with no spec change. Reserve **hardcoded** values for the one or two properties you want pixel-locked (a brand color, a fixed font size). Rule of thumb: *token = inherits the publisher's styling, hardcoded = locked*.

### Start from a safe default card

A good starting spec is a minimal, fully tokenized card — structure fixed, everything else inherited — which looks native out of the box and is hard to make ugly:

```json theme={null}
{
  "version": 1,
  "name": "default-card",
  "root": {
    "type": "link",
    "style": { "display": "block", "background": "hsl(var(--card))", "color": "hsl(var(--card-foreground))", "border": "1px solid hsl(var(--border))", "borderRadius": "var(--radius)", "padding": "16px" },
    "children": [
      { "type": "box", "style": { "display": "flex", "alignItems": "center", "gap": "8px", "marginBottom": "8px" }, "children": [
        { "type": "image", "bind": "favicon", "showIf": "favicon", "style": { "width": "16px", "height": "16px", "borderRadius": "4px" } },
        { "type": "text", "bind": "brandName", "style": { "fontSize": "13px", "color": "hsl(var(--muted-foreground))" } }
      ] },
      { "type": "text", "bind": "title", "style": { "display": "block", "fontSize": "16px", "fontWeight": 600 } },
      { "type": "text", "bind": "adText", "showIf": "adText", "style": { "display": "block", "fontSize": "14px", "color": "hsl(var(--muted-foreground))", "marginTop": "4px" } },
      { "type": "text", "bind": "cta", "style": { "display": "inline-block", "marginTop": "12px", "fontSize": "14px", "fontWeight": 600, "color": "hsl(var(--primary))" } }
    ]
  }
}
```

### Building your own renderer? Match these impression rules

If you interpret `renderer_spec` with your own runtime instead of `@gravity-ai/ui`, fire tracking the same way our SDK does so your numbers reconcile with the Gravity dashboard:

* **Impression** — request `impUrl` exactly **once per served ad**, when the unit first reaches **50% visibility** in the viewport (`IntersectionObserver` with `threshold: 0.5`). Don't fire on mount/render.
* **Reset per ad** — if the slot receives a new ad (new `impUrl`), the once-per-ad rule resets for the new ad.
* **No observer available** (SSR hydration edge cases, old browsers) — fall back to firing once on mount.
* **Clicks** — route every click through `clickUrl`, never the raw `url`.

Gravity's dashboard counts an impression when `impUrl` is requested, so a renderer following these rules will match the dashboard 1:1.

### Want a custom default look for your placement?

Gravity can pin a custom spec as a placement's **default look** so every ad in that slot renders it with no per-call config — designed with you, previewed against real ads on your surface before it goes live. Email [support@trygravity.ai](mailto:support@trygravity.ai) with the look you want (or a spec you've drafted) and we'll set it up.

<Tip>
  A custom default never changes tracking or attribution — only the visual DOM. Impressions and clicks fire identically regardless of which spec renders.
</Tip>

## Security

The spec is data, not code. The runtime cannot execute anything:

* **Fixed tag whitelist** — `type` only maps to `<div>`, `<span>`, `<img>`, `<a>`.
* **No code execution** — no `eval`, `new Function`, `dangerouslySetInnerHTML`, or event handlers. Text is auto-escaped.
* **Sanitization** — style values with `javascript:`/`expression(` are stripped; `src`/`href` must use an allowed protocol (`http`, `https`, `data`, `mailto`).
* **Bounded work** — tree depth and node count are capped.
* **Scoped data** — `bind` reads only the six known ad fields.

## FAQ

<AccordionGroup>
  <Accordion title="Do publishers upgrade the SDK when we ship a new design?">
    No. New designs are delivered as `renderer_spec` data on the ad response. The installed runtime renders them as-is.
  </Accordion>

  <Accordion title="Will the ad look native in our app?">
    Yes, if the spec references theme tokens. ShadCN/Tailwind apps inherit automatically; others supply tokens via `GravityThemeProvider`.
  </Accordion>

  <Accordion title="Can the publisher override what we serve?">
    Yes — a `variant` prop hard-pins a bundled layout and ignores the server spec.
  </Accordion>

  <Accordion title="Do publishers have to author JSON to get a good-looking ad?">
    No. With no spec at all, the SDK renders its built-in, dimension-aware look that inherits your theme. Authoring a spec is only for when you want a specific custom look.
  </Accordion>

  <Accordion title="If we restyle our app, do we have to re-edit the spec?">
    No, as long as the spec uses theme tokens (`hsl(var(--primary))`, `var(--radius)`). Those resolve against your CSS at paint time, so the ad re-themes automatically. Only hardcoded values stay fixed.
  </Accordion>

  <Accordion title="Does a custom design change impressions, clicks, or billing?">
    No. A spec only changes the visual DOM. Impression and click tracking route through the same `impUrl`/`clickUrl` regardless of which spec renders, so attribution and billing are unchanged.
  </Accordion>
</AccordionGroup>
