Installation
GRAVITY_API_KEY in your server environment.
How the pieces fit together
Publisher — drop in the component
Place
<GravityAd /> where the ad should appear. For a ShadCN/Tailwind app that’s the entire integration.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.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.Render ads
| 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. |
IntersectionObserver.
Theme
ShadCN/Tailwind apps need nothing — the ad reads their existing CSS variables and follows light/dark mode. Other apps wrap once withGravityThemeProvider:
Renderer spec
The ad response carries arenderer_spec — the JSON tree the SDK renders. Render precedence:
- Publisher
variantprop — a hard pin. Always wins. - Server
renderer_spec— the normal path. Gravity ships the look remotely. - 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.
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.
A spec for a single line of text:
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. |
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 thead 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.
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.
Styling
Thestyle 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:
--background, --foreground, --card, --primary, --muted-foreground, --accent, --border, --radius.
Hardcode values for pixel-perfect control. Any CSS property works:
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
Arenderer_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:Building your own renderer? Match these impression rules
If you interpretrenderer_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
impUrlexactly once per served ad, when the unit first reaches 50% visibility in the viewport (IntersectionObserverwiththreshold: 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 rawurl.
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 with the look you want (or a spec you’ve drafted) and we’ll set it up.Security
The spec is data, not code. The runtime cannot execute anything:- Fixed tag whitelist —
typeonly 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/hrefmust use an allowed protocol (http,https,data,mailto). - Bounded work — tree depth and node count are capped.
- Scoped data —
bindreads only the six known ad fields.
FAQ
Do publishers upgrade the SDK when we ship a new design?
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.Will the ad look native in our app?
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.Can the publisher override what we serve?
Can the publisher override what we serve?
Yes — a
variant prop hard-pins a bundled layout and ignores the server spec.If we restyle our app, do we have to re-edit the spec?
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.Does a custom design change impressions, clicks, or billing?
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.
