adkit-react
v1.1.2
Published
A drop-in React component for selling direct, bookable ads on your site — no AdSense required.
Downloads
102
Maintainers
Readme
adkit-react
A drop-in React component for selling direct, bookable ad space on your site — no AdSense, no programmatic networks, no middlemen.
Not using React? See adkit-js for the vanilla JavaScript SDK.
Overview
Add <AdkitProvider> and <AdSlot> to your app. Each slot either renders a paid advertiser's creative or a "Rent this spot" placeholder that lets visitors book directly from your page. You set a flat daily price. Adkit handles the booking flow, payment, and approval process.
Installation
npm install adkit-reactQuick Start
import { AdkitProvider, AdSlot } from "adkit-react"
import "adkit-react/styles.css"
function App() {
return (
<AdkitProvider siteId="your-site-id">
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} />
</AdkitProvider>
)
}Your site ID is found in the Adkit dashboard. The slot above goes live at $25/day immediately.
Setup
1. Wrap your app with <AdkitProvider>
import { AdkitProvider } from "adkit-react"
import "adkit-react/styles.css"
export default function RootLayout({ children }) {
return (
<AdkitProvider siteId="your-site-id">
{children}
</AdkitProvider>
)
}The provider shares your site ID across all slots in its subtree and manages the refresh lifecycle. You only need one provider per site.
2. Place <AdSlot> components where you want ad placements
import { AdSlot } from "adkit-react"
export default function Sidebar() {
return (
<aside>
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} />
</aside>
)
}3. Import the stylesheet
import "adkit-react/styles.css"Import this once at the root of your app. It covers all slot and modal styles.
Components
<AdkitProvider>
Wraps your app (or any subtree) to provide site ID context and slot lifecycle management to all child <AdSlot> components.
<AdkitProvider siteId="your-site-id">
{children}
</AdkitProvider>| Prop | Type | Required | Description |
|---|---|---|---|
| siteId | string | Yes | Your Adkit site ID from the dashboard |
| children | ReactNode | Yes | Your app or subtree |
<AdSlot>
Renders a single ad placement. Fetches its status from the Adkit API and renders either an active ad or a booking placeholder.
<AdSlot
slot="sidebar"
aspectRatio="4:3"
price={2500}
size="lg"
theme="auto"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| slot | string | — | Required. Unique placement name. Letters, numbers, hyphens, underscores only. |
| aspectRatio | AspectRatio | — | Required. Slot shape — see Aspect Ratios. |
| price | number | — | Required. Daily price in cents (e.g. 2500 = $25/day). See Pricing. |
| siteId | string | — | Manual site ID override. Not needed inside <AdkitProvider>. |
| size | "sm" \| "md" \| "lg" | "lg" | Placeholder text size. |
| theme | "light" \| "dark" \| "auto" | "auto" | Color theme. "auto" follows system preference. |
| className | string | — | CSS class(es) applied to the slot container. Use this to set width. |
| styles | AdSlotStyles | — | Custom color overrides for the placeholder. Does not affect rendered ads. |
| silent | boolean | false | Disable all analytics event tracking for this slot. |
Without a Provider
You can use <AdSlot> without a provider by passing siteId directly:
<AdSlot
siteId="your-site-id"
slot="sidebar"
aspectRatio="4:3"
price={2500}
/>This is useful for embedding a single slot without modifying your app's root.
<BookingModal>
The booking modal is opened automatically when a visitor clicks a placeholder. It's also exported for advanced use cases where you need to trigger it programmatically.
import { BookingModal } from "adkit-react"
<BookingModal
siteId="your-site-id"
slot="sidebar"
price={2500}
onClose={() => setOpen(false)}
/>| Prop | Type | Description |
|---|---|---|
| siteId | string | Your Adkit site ID |
| slot | string | Slot name |
| price | number | Daily price in cents (optional — modal renders without a price if omitted) |
| onClose | () => void | Called when the modal is dismissed |
The modal closes on Escape key, backdrop click, or the Cancel button. Body scroll is locked while it's open. The "Book this ad" CTA redirects to https://adkit.dev/book?siteId=...&slot=...&ref=.... The price is not included in the URL — the booking page fetches it server-side to prevent manipulation.
Aspect Ratios
| Value | Ratio | Best for |
|---|---|---|
| "16:9" | 16:9 | Hero banners, video-style placements |
| "4:3" | 4:3 | Sidebars, content blocks |
| "1:1" | 1:1 | Square placements, social-style |
| "9:16" | 9:16 | Vertical/mobile, stories format |
| "banner" | 728:90 | Leaderboard banners |
The aspect ratio is enforced by the component via CSS aspect-ratio. Set width via className — height is always derived automatically.
// Width via Tailwind
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} className="w-[300px]" />
// Width via inline style
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} style={{ width: 300 }} />Pricing
Setting a Price
Pass price in cents. The first time the slot mounts, it's registered at that price and becomes immediately bookable.
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} /> {/* $25/day */}
<AdSlot slot="hero" aspectRatio="16:9" price={10000} /> {/* $100/day */}Changing a Price
Update the price prop and redeploy. The SDK sends the new value on the next slot_mount event.
- Price increases — applied immediately, no confirmation required
- Price decreases — you'll receive a confirmation via email or dashboard notification before the change takes effect, preventing accidental or manipulated price reductions
You can also change prices directly in the Adkit dashboard. Dashboard changes take effect immediately in either direction.
How Pricing Works Under the Hood
The price prop has two roles:
- During loading — displayed in the placeholder while the API fetch is in-flight, so the slot shows a price rather than a blank state
- After API response — replaced by the server-authoritative price from the API response
The server price always wins once the fetch resolves. Billing always uses the confirmed database price.
Slot Behavior
Loading State
On mount, the slot immediately renders with "ad space" as the label and your price prop as a hint. This prevents layout shift — the slot occupies its full dimensions before the API responds.
Active Ad State
When a booking is active, the slot renders the advertiser's creative as a full-size image linked to their destination URL. Impressions are tracked at 50% viewport visibility. Clicks are tracked on the link. If the image fails to load, the slot automatically falls back to placeholder state.
Placeholder State
When no booking is active, the slot renders a dashed-border card with "Your ad here", the server-confirmed daily price, and a "Rent this spot" CTA. Clicking opens the booking modal. Banner slots show a compact "Rent" label to fit the narrow format.
Theming
Built-in Themes
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} theme="light" />
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} theme="dark" />
<AdSlot slot="sidebar" aspectRatio="4:3" price={2500} theme="auto" /> {/* default */}"auto" subscribes to prefers-color-scheme changes and re-renders automatically when the user switches system theme.
Custom Colors
Use the styles prop to override placeholder colors per-slot:
<AdSlot
slot="branded"
aspectRatio="1:1"
price={3000}
styles={{
backgroundColor: "#fef3c7",
textColorPrimary: "#92400e",
textColorSecondary: "#d97706",
borderColor: "#f59e0b",
}}
/>| Field | Description |
|---|---|
| backgroundColor | Placeholder background (default: transparent) |
| textColorPrimary | Price text color |
| textColorSecondary | Label and CTA text color |
| borderColor | Dashed border color (rendered at 40% opacity, 60% on hover via color-mix()) |
These styles apply to the placeholder only. When an active ad is displayed, the advertiser controls their creative.
Border color requires color-mix() support: Chrome 111+, Firefox 113+, Safari 16.2+.
Size Presets
The size prop scales the label, price, and CTA text together:
| Value | Best for |
|---|---|
| "sm" | Compact slots under ~200px wide |
| "md" | Standard slots 200–400px wide |
| "lg" | Large/hero slots over 400px wide (default) |
Hooks
useAdkit()
Access the Adkit context from any component inside <AdkitProvider>. Throws if called outside a provider — this is intentional, as missing a provider is a developer error.
import { useAdkit } from "adkit-react"
function RefreshButton() {
const { refresh } = useAdkit()
return <button onClick={refresh}>Refresh ads</button>
}Context values
| Value | Type | Description |
|---|---|---|
| siteId | string | The site ID passed to the provider |
| refresh | () => void | Re-fetches all slots in the subtree |
| refreshKey | number | Increments on each refresh — used internally by <AdSlot> |
refresh()
Calling refresh() does the following:
- Clears the registered slot set (duplicate detection resets)
- Clears the mounted slots set (mount events re-fire)
- Increments
refreshKey, triggering a re-fetch in every<AdSlot>in the subtree
Use this after SPA navigation or any time you want slots to re-check their booking status.
// Re-fetch on route change
const { refresh } = useAdkit()
const pathname = usePathname()
useEffect(() => {
refresh()
}, [pathname])Analytics Events
The SDK sends four event types to https://adkit.dev/api/events. All events use navigator.sendBeacon() with a fetch fallback. Errors are silently swallowed — analytics never break your app.
slot_mount
Fires once per slot identity per page load, on mount (before the API responds). Carries the price prop to register or update the slot's price on the server.
{
"type": "slot_mount",
"siteId": "your-site-id",
"slot": "sidebar",
"pathname": "/blog/my-post",
"price": 2500,
"aspectRatio": "4:3",
"timestamp": 1743264000000
}price is omitted if the prop is not set.
slot_view
Fires when 50% of the slot enters the viewport via IntersectionObserver. Fires for both active ads and placeholders — this enables fill rate calculation. Fires at most once per slot per page load.
{
"type": "slot_view",
"slotId": "your-site-id:sidebar",
"bookingId": "booking-abc123",
"pathname": "/blog/my-post",
"viewport": "1440x900",
"timestamp": 1743264000000
}bookingId is only present when an active ad is displayed.
slot_click
Fires when a visitor clicks an active ad. Does not fire for placeholder clicks.
{
"type": "slot_click",
"slotId": "your-site-id:sidebar",
"bookingId": "booking-abc123",
"pathname": "/blog/my-post",
"viewport": "1440x900",
"timestamp": 1743264000000
}slot_duplicate
Fires when two <AdSlot> components share the same siteId:slot identity on the same page. Both slots still render.
{
"type": "slot_duplicate",
"siteId": "your-site-id",
"slot": "sidebar",
"pathname": "/blog/my-post",
"timestamp": 1743264000000
}Disabling Tracking
<AdSlot slot="test" aspectRatio="1:1" price={500} silent />Error Handling
The SDK throws explicit errors for developer misconfiguration so required props fail fast during development and testing.
| Scenario | Behavior |
|---|---|
| Missing slot, aspectRatio, or price | Throws |
| Invalid slot name | Throws |
| Missing siteId (no provider, no prop) | Throws |
| useAdkit() called outside provider | Throws — this is a developer error |
| Duplicate slot identity | Both render, console.warn + analytics event |
| API fetch timeout (5s) | Falls back to placeholder |
| API non-200 or network failure | Falls back to placeholder |
| Ad image load failure | Falls back to placeholder |
Next.js
adkit-react is fully compatible with the App Router. All components are marked "use client" at the bundle level — you don't need to add the directive yourself.
// app/layout.tsx
import { AdkitProvider } from "adkit-react"
import "adkit-react/styles.css"
export default function RootLayout({ children }) {
return (
<html>
<body>
<AdkitProvider siteId="your-site-id">
{children}
</AdkitProvider>
</body>
</html>
)
}// app/components/Sidebar.tsx — no "use client" needed
import { AdSlot } from "adkit-react"
export default function Sidebar() {
return <AdSlot slot="sidebar" aspectRatio="4:3" price={2500} />
}Pages Router
// pages/_app.tsx
import { AdkitProvider } from "adkit-react"
import "adkit-react/styles.css"
export default function App({ Component, pageProps }) {
return (
<AdkitProvider siteId="your-site-id">
<Component {...pageProps} />
</AdkitProvider>
)
}TypeScript
Full TypeScript support is included. Types are exported from the package root.
import type { AspectRatio, AdSlotProps, AdSlotStyles } from "adkit-react"
const ratio: AspectRatio = "16:9"
const customStyles: AdSlotStyles = {
borderColor: "#3b82f6",
backgroundColor: "transparent",
textColorPrimary: "#1a1a1a",
textColorSecondary: "#666",
}Slot Identity
A slot's identity is siteId:slot — the combination of your site ID and the slot name. This identity is not page-scoped. A slot named "sidebar" on /blog/post-1 and /blog/post-2 is the same placement, and a single booking covers both pages.
This is intentional: when an advertiser rents your sidebar, they expect their ad to appear everywhere your sidebar appears — not just on one URL.
If you need page-specific placements, use distinct slot names:
<AdSlot slot="homepage-hero" aspectRatio="16:9" price={5000} />
<AdSlot slot="blog-sidebar" aspectRatio="4:3" price={2500} />Content Security Policy
If your site uses CSP headers, add:
connect-src https://adkit.dev;
img-src https://ufs.sh;connect-src covers the serve API (/api/serve) and analytics (/api/events). img-src covers ad creatives stored on UploadThing.
Browser Support
| Browser | Minimum Version | |---|---| | Chrome | 88+ | | Firefox | 89+ | | Safari | 15+ | | Edge | 88+ |
Required APIs: IntersectionObserver, ResizeObserver, fetch, CSS aspect-ratio.
borderColor in styles requires color-mix(): Chrome 111+, Firefox 113+, Safari 16.2+.
License
MIT
