flowgrid-sdk
v2.1.2
Published
A TypeScript SDK for tracking user events, feature usage, experiments, and feature flags with Flowgrid.
Maintainers
Readme
Flowgrid SDK
A production-grade TypeScript SDK for product analytics, feature flags,
experiments, and enterprise insights. First-class bindings for React and
Next.js; the core (track, flags, identify) is plain JavaScript and
runs in any browser, Node or edge runtime.
Start for free: [https://www.flow-grid.xyz/]
import { FlowGrid } from "flowgrid-sdk";
// One line. Sane defaults. Auto-instruments sessions, page views,
// performance, heatmaps, and attribution out of the box.
const fg = FlowGrid.init({
webId: "wx_123",
apiKey: "pk_xxx",
// Opt in to the extras you want at init.
autoTrack: {
replay: true,
engagement: true,
performance: true,
passiveIdentity: true,
},
});
await fg.track("signup_completed", { plan: "pro" });
await fg.track("feature_used", { featureId: "export", featureName: "Data Export", userId: "u_1" });
await fg.track("experiment_exposure", { experimentId: "checkout_v2", variantId: "B", userId: "u_1" });
await fg.track("add_to_cart", { productId: "sku_1", name: "T-Shirt", price: 29, currency: "USD", quantity: 1 });Highlights
- One class, every feature —
new FlowGrid(...)exposes analytics, experiments, growth and enterprise modules as lazy properties. - Feature flags in one line —
flags.on('new-checkout'). Synchronous, works offline, never throws; an unknown flag returns the default you pass. Declare flags in code and they appear in the dashboard, switched off. - Feature usage, explicit —
fg.track("feature_usage", { featureId, featureName }). You say what happened; the SDK just sends it. - Passive identity — opt in at init with
autoTrack: { passiveIdentity: true }(or later viafg.autoTrack({ passiveIdentity: true })) to recognise anonymous visitors from any form (PII hashed in the browser). - React & Next.js bindings — hooks, components and Server Component reads.
reactis the only (optional) peer dependency; the core runs in any browser, Node or edge runtime. - Privacy & consent built-in — DNT/GPC support,
ConsentManager, bot filtering. - Resilient transport — retry with exponential backoff,
sendBeaconfallback, offlinelocalStoragebuffering, 10KB payload guard. - Strict TypeScript — strong types for every config and response.
Installation
npm install flowgrid-sdk
# or
pnpm add flowgrid-sdk
# or
yarn add flowgrid-sdkRequires Node >= 18.12. No peer dependencies.
Quick Start
FlowGrid is one platform with two runtimes. Initialise once per runtime — the browser client for everything a visitor does on the page, the server client for what happens in your backend — and both feed the same dashboard under the same identity:
// lib/flowgrid.client.ts — BROWSER: init once, import everywhere client-side
import { FlowGrid } from "flowgrid-sdk";
export const fg = FlowGrid.init({
webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!, // public site id (wx_…)
apiKey: process.env.NEXT_PUBLIC_FLOWGRID_API_KEY!, // public API key
// Who is this? (optional — identifies up-front so the very first events attribute)
user: { userId: "u_1", email: "[email protected]", name: "Alice" },
// What gets auto-tracked? engagement + replay are on by default;
// pageviews/sessions/heatmaps/performance come from the script tag install.
autoTrack: {
engagement: true,
replay: { sampleRate: 1.0, maskAllInputs: true },
passiveIdentity: true, // recognise visitors from forms (PII hashed in-browser)
},
// What is the visitor consenting to? (see “Cookie Consent” below —
// pair with renderConsentBanner() for an opt-in banner in one call)
consent: { analytics: true, marketing: true },
});// lib/flowgrid.server.ts — SERVER (Node ≥ 18 / edge): identity, feature usage, revenue, errors
import { FlowGridServer } from "flowgrid-sdk/server";
export const fgServer = FlowGridServer.init({
webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!, // same site id
apiKey: process.env.FLOWGRID_API_KEY!, // private key — server env only
});That's the whole setup. Browser calls (fg.track, fg.identifyUser,
fg.flags.on, …) and server calls (fgServer.trackAuth,
fgServer.trackFeature, fgServer.trackSubscription, fgServer.trackError, …) speak the same wire
contract, so a user identified in an OAuth callback on the server and the
same user clicking around in the browser resolve to one identity in the
dashboard.
Unified client (browser)
import { FlowGrid } from "flowgrid-sdk";
const fg = FlowGrid.init({
webId: "web_123",
apiKey: "key_xxx",
autoTrack: { heatmaps: { movement: true } },
});
// Plain custom events still work.
await fg.track("cta_click", { id: "hero" }, { userId: "u_1" });
// Feature usage — a plain event.
fg.track("feature_usage", { featureId: "data_export", featureName: "Data Export" });
// Recognized event names route to the matching feature module.
await fg.track("feature_used", { featureId: "export", featureName: "Data Export", userId: "u_1" });
await fg.track("prompt_submitted", { promptId: "p_1", promptType: "chat", userId: "u_1" });
await fg.track("experiment_conversion", { experimentId: "checkout_v2", variantId: "B", userId: "u_1", metricName: "purchase" });
await fg.track("support.ticket_created", { ticketId: "t_1", userId: "u_1", subject: "Help", category: "billing", priority: "medium", channel: "email" });Available namespaces on FlowGrid:
| Group | Properties |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| Core | activation, features, prompts, experiments, flags |
| Analytics | pageViews, sessions, events, identify, funnels, retention, attribution, heatmaps, performance, replay |
| Growth | subscriptions |
| Enterprise | engagement, cohorts, churn, monetization, multiPathFunnels, support, acquisition, paths, alerts, security, forecasting |
Browser script tag (CMS / no-build)
<script src="https://cdn.flow-grid.xyz/flowgrid.min.js"></script>
<script>
FlowGrid.init({
webId: "web_abc123",
apiKey: "key_xxx",
});
FlowGrid.track("signup_clicked", { placement: "hero" });
FlowGrid.identify("user_123", { plan: "pro" });
FlowGrid.track("feature_usage", { featureId: "ai_agent_builder", featureName: "AI Agent Builder" });
FlowGrid.track("add_to_cart", { productId: "sku_1", name: "T-Shirt", price: 29, quantity: 1 });
</script>The script-tag API is intentionally small: init, track, page, identify,
signup, login, defineFeature, experiment, autoTrack, consent helpers
(setConsent / hasConsent / wireConsentBanner), reset, and instance() for the
full SDK. FlowGrid.init() auto-instruments sessions, page views, SPA route
changes, scroll depth, time on page, performance, heatmaps, and attribution by
default; session replay is opt-in with autoTrack: { replay: true }, and passive
identity capture with autoTrack: { passiveIdentity: true }.
Feature Usage
Track product feature usage with a plain event — featureId and featureName
are the only required props:
fg.track("feature_usage", { featureId: "ai_agent_builder", featureName: "AI Agent Builder" });
// Optional: an action ("viewed" | "used" | "completed" | "abandoned"), a category, a known user.
fg.track("feature_usage", {
featureId: "ai_agent_builder",
featureName: "AI Agent Builder",
action: "completed",
category: "automation",
userId: "u_1",
});
// script tag: FlowGrid.track("feature_usage", { featureId: "ai_agent_builder", featureName: "AI Agent Builder" })action defaults to "used". No userId or category is required — it works
for anonymous visitors out of the box (the backend defaults userId to the
visitor and category to "general"). The event routes through the unified
.track() router, so it shares routing/consent/transport with every other event.
Helpers
Don't want to hand-write track("feature_usage", …) and repeat the id/name?
Bind a feature once with fg.defineFeature and call the verb that describes
what happened — each maps to a lifecycle stage the dashboard funnel understands
(a viewed feature is Discovered, a used one is Adopted):
const feature = fg.defineFeature("ai-agent-builder", "AI Agent Builder", { category: "automation" });
feature.viewed(); // action: "viewed" → Discovered + Viewed
feature.used({ model: "opus" }); // action: "used" → Adopted
feature.completed(); // action: "completed" → Completed
feature.abandoned(); // action: "abandoned" → Abandonment
// script tag: FlowGrid.defineFeature("ai-agent-builder", "AI Agent Builder").used()Each call is a thin, explicit wrapper over track("feature_usage", …) — no
hidden state, no lifecycle guessing; you send exactly the action you name. The
same verbs exist on fg.features.viewed/used/completed/abandoned(id, name) for
one-off calls, and fg.features.record(id, name, action) for a dynamic action.
Feature Flags
Ship code with a feature turned off, then turn it on from the dashboard — for everyone, for a percentage, or for one customer. No deploy, no restart.
import { flags } from 'flowgrid-sdk';
if (flags.on('new-checkout')) {
renderNewCheckout();
}That is the whole integration. There is nothing to register and nothing to
configure: FlowGrid.init() already loaded your flags.
The three rules
on()is synchronous. Rules are already in memory, so a flag read is a map lookup — safe on a render path, noawait, no loading state.The second argument is the default, not an options bag.
flags.on('x', true)reads as "on unless told otherwise". Flag not created, bundle not loaded, network down, SDK blocked by an ad blocker — you get your default. Never an exception, never a hang.Create flags in the dashboard or in code. List keys in
flags.declareand any your website doesn't have yet appear in the dashboard, switched off. Declaring never turns a flag on or changes an existing flag, so it is safe to repeat. Keys are used exactly as written.FlowGrid.init({ webId, flags: { declare: ['new-checkout'] } }); await getServerFlags({ webId, declare: ['new-checkout'] }); // server
Framework bindings
Same contract everywhere, and the same shape as useExperiment:
// React
import { useFlag, Flag } from 'flowgrid-sdk/react';
const enabled = useFlag('new-checkout');
return enabled ? <NewCheckout /> : <Checkout />;
// or declaratively
<Flag name="new-checkout" fallback={<Checkout />}>
<NewCheckout />
</Flag>// Next.js — Server Component. No flash, no hydration mismatch.
import { getServerFlag } from 'flowgrid-sdk/server';
const enabled = await getServerFlag('new-checkout', {
webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!,
context: { userId: session?.user.id },
});Errors from a flagged feature
A flag's error rate counts only errors the flagged code throws — never
page-wide browser errors. <Flag> catches them for you; elsewhere use
flags.run(). If the new path throws, the error is reported and the old path
runs instead.
const total = await flags.run('new-pricing', () => priceV2(cart), () => price(cart));
flags.reportError('new-pricing', error); // anything else
fg.trackError(error, { flag: 'new-pricing', visitorId }); // serverTargeting
Rules live in the dashboard, not in your bundle, so changing who gets a feature never touches this code. Everything the SDK knows about the visitor is available to a rule:
FlowGrid.init({
webId: '…',
// Optional. `environment` is worked out from the host you're on — localhost
// and LAN addresses are `development`, per-branch preview URLs and a leading
// `staging.`/`qa.` are `preview`, everything else is `production`. Pass it
// yourself only when you know better than the hostname does.
flagContext: { environment: process.env.NEXT_PUBLIC_VERCEL_ENV },
});
// Every trait here becomes an attribute you can target a rule on — plan, org,
// role, whatever your product actually has. User rules key off the id.
fg.identifyUser('u_123', { plan: 'pro', org: 'acme' });In the browser, visitorId, environment and sdkVersion are filled in for
you, so percentage rollouts split anonymous traffic and environment/SDK-version
rules work with no setup. On a server, pass visitorId (from
visitorIdFromCookies()) and environment to getServerFlags yourself. region you cannot pass
meaningfully and shouldn't try — a browser has no honest answer, so a flag with
a region rule is decided on FlowGrid's side from the request itself. You read it
exactly like any other flag.
Each rule reads exactly one field, and a rule whose input you didn't provide simply doesn't match — so a forgotten field under-enables rather than leaking an unreleased feature.
Percentage rollouts are sticky and monotonic. A visitor's bucket never moves, and raising 10% → 20% only ever adds people — nobody loses the feature mid-rollout. Server and browser bucket identically, so an SSR render and its hydration agree.
They also work on anonymous traffic: the SDK buckets on the visitor id it
already keeps, so you don't need logins to roll out gradually. After sign-in,
bucketing moves to the userId you pass, so a rollout follows one person across
devices — which can change whether that individual is in the slice.
Rules don't AND, so "50% of pro customers" is two flags — one for the audience, one for the pace:
// 'pro-beta-eligible' → Attribute: plan is one of [pro]
// 'new-checkout' → Percentage: 25%
if (flags.on('pro-beta-eligible') && flags.on('new-checkout')) { /* … */ }Flags that target individual people are never evaluated in the browser. The rule is the target list, and shipping it would hand that list to anyone with devtools. Those flags resolve on our servers and only the resulting boolean crosses the wire — you don't have to do anything to get this; it's decided when the flag is compiled.
Any framework, in four lines
The shipped React and Next bindings are conveniences, not privileged
access. Underneath, a flag is a subscribable store — subscribe(listener)
returning an unsubscribe, with the listener called immediately — which is the
shape every reactive framework already knows how to consume:
// Solid
const [on, setOn] = createSignal(false);
onCleanup(fg.flags.store('new-checkout').subscribe(setOn));
// Angular
readonly on = toSignal(from(fg.flags.store('new-checkout')), { initialValue: false });
// Qwik
useVisibleTask$(({ cleanup }) => cleanup(fg.flags.store('new-checkout').subscribe((v) => (sig.value = v))));
// Vanilla, Alpine, Lit, htmx, a web component, anything
fg.flags.store('new-checkout').subscribe((on) => { el.hidden = !on; });If you don't need reactivity at all, flags.on('key') is a synchronous boolean
and works in any JavaScript that runs — Node, Deno, Bun, Cloudflare Workers, a
Chrome extension, a build script.
Without the rest of the SDK
Flags don't require FlowGrid's analytics. If you already have analytics, or you're in a runtime that has no use for it:
import { createFlags } from 'flowgrid-sdk';
const flags = createFlags({ webId: '…', context: { environment: 'production' } });
await flags.ready();
if (flags.on('new-checkout')) { … }Same engine, same guarantees.
Typed keys, without a code generator
// flags.ts
import { defineFlags } from 'flowgrid-sdk';
export const flags = defineFlags(['new-checkout', 'dark-mode'] as const);
// anywhere
flags.on('new-checkout'); // ✅
flags.on('new-chekcout'); // ❌ compile errorReacting to changes
await flags.ready(); // resolves once flags have loaded (or failed)
const stop = flags.onChange(rerender);The framework bindings do this for you.
Caching, and debugging a wrong answer
Flags are fetched once per page load and kept for a day, so a returning visitor evaluates correctly on the first render — before the network answers, and correctly enough with none at all. That is the default and most apps should leave it. It is a trade, though: a cached answer can be up to a day stale for one round trip, which is a bargain on a marketing page and unacceptable on some others.
Set this in your FlowGrid dashboard (Feature flags → Delivery). It travels
with your flags, so changing it takes effect on your visitors' next load with no
deploy of your app — which is the point. The options below configure the SDK for
the case where you have not set it in the dashboard; the dashboard wins
whenever it has an opinion, and flags.lookupPolicySource tells you which is in
force.
FlowGrid.init({
webId: '…',
flags: {
// 'persistenceUntilNetworkSuccess' (default) — the cached answer renders
// immediately and is replaced when the fetch lands.
// 'networkFirst' — your own default renders until the fetch lands; the
// cache is used only if it fails.
// 'networkOnly' — nothing is stored. A failed fetch means every flag is
// the default you passed.
lookupPolicy: 'networkFirst',
persistenceTtlMs: 60 * 60 * 1000, // default: 24h
},
});Two conveniences come from the stored copy, and networkOnly gives up both: a
visitor with no connection has no flags, and a route you have excluded from flag
delivery can no longer be recognised locally, so every load on it asks FlowGrid
instead of answering for free.
When a flag isn't doing what you expect, explain() answers both halves — which
step decided, and which copy of your flags it decided against:
flags.explain('new-checkout');
// {
// value: false,
// variant: null,
// reason: 'default', // kill_switch | rule | default | unknown_flag
// source: 'persistence', // network | persistence | resolve | fallback
// }flags.lookupPolicy(); // 'networkFirst'
fg.flags.lookupPolicySource; // 'dashboard' — your init config is being overriddenreason is why; source is from where. resolve means the flag's rules
were withheld from the browser on purpose and our server answered the key
directly; fallback means there was no answer here at all and you got your own
default. The same reason off a day-old cache and off a fresh fetch are the
same reasoning and completely different bugs, which is why both are reported.
Passive Identity Capture
Recognize anonymous visitors automatically: when a visitor types their email /
name / phone into any form, FlowGrid classifies the field, hashes PII in the
browser, and links the identity to their visitor_id — so the dashboard shows
them by name (and, with a CRM connected, matches them to the contact). No
per-form instrumentation.
It's opt-in in the SDK (the flowgrid.js snippet runs it by default) and
requires analytics consent:
const fg = FlowGrid.init({ webId: "...", apiKey: "..." });
FlowGrid.setConsent({ analytics: true });
const stop = fg.autoTrack({ passiveIdentity: true });
// or with options:
fg.autoTrack({ passiveIdentity: { debounceMs: 800, excludePath: /\/(checkout|admin)/i } });Never reads password/hidden/card/cvv/ssn/pin fields, anything marked
data-analytics="ignore" or .no-track, free-text (message/comment/search), or
sensitive paths (/checkout, /account/settings, /admin). Raw email goes only
to the server-side identity store; the event stream sees hashes.
Server-side / Edge Usage
Use the dedicated flowgrid-sdk/server entry point on the server. It is a
Node/edge-safe client (global fetch only — no window, document, or
storage) scoped to what makes sense off-browser: identity, feature
usage, subscriptions & revenue attribution, feature flags, and API / server error tracking. It speaks the same wire contract
as the browser SDK, so events land in the same pipes. Works in Node ≥ 18, Bun,
Deno, Cloudflare Workers, and Vercel Edge/serverless.
// lib/flowgrid-server.ts — init once, import anywhere server-side
import { FlowGridServer } from "flowgrid-sdk/server";
export const fg = FlowGridServer.init({
webId: process.env.NEXT_PUBLIC_WEB_ID!,
apiKey: process.env.FLOWGRID_API_KEY!, // required on the server
});Identity — auth-level events (identify a visitor server-side, e.g. in an
OAuth callback or session endpoint). Pass the browser's visitor id when you
have the request cookies so server events link to the web session; without it,
a stable synthetic visitor is derived from the userId:
import { visitorIdFromCookies } from "flowgrid-sdk/server";
import { cookies } from "next/headers";
const visitorId = visitorIdFromCookies(await cookies());
await fg.trackAuth(
{ event: "login", userId: user.id, email: user.email, method: "google" },
{ visitorId },
); // identify + login_completed + active_user ping, in one call
// Or the individual operations:
await fg.identifyUser(user.id, { email: user.email, plan: "pro" }, { visitorId });
await fg.updateTraits(user.id, { plan: "enterprise" });
await fg.pingActiveUser(user.id, { email: user.email }); // DAU/WAU/MAU by identity
await fg.logout(user.id);Feature usage from API routes, jobs, and webhooks:
await fg.trackFeature({ featureId: "csv_export", featureName: "CSV Export", userId: user.id });
// Or bind once and use semantic verbs (server twin of fg.defineFeature):
const teamUpdate = fg.defineFeature("team_update", "Team update", { category: "teams" });
await teamUpdate.used({ userId: user.id, teamId });Revenue — purchases and refunds are not SDK calls. Connect your payment provider in Flowgrid (Website settings → Payments) and Flowgrid receives its webhook directly, where the money is actually confirmed. What the SDK owns is the subscription / MRR lifecycle, sent from your billing webhooks:
// Subscription / MRR lifecycle — one method, discriminated on `event`:
await fg.trackSubscription({
event: "start", // "change" | "cancel" | "renew" | "trial_converted" | "trial_expired"
subscriptionId: sub.id,
userId,
plan: "professional",
mrr: { amount: 9900, currency: "USD" }, // smallest currency unit
billingCycle: "monthly",
trialDays: 14,
});Revenue attribution — payment webhooks arrive with no cookies, so the visitor/session ids ride through the provider's checkout metadata and come back out in the webhook. Two legs:
// LEG 1 — checkout creation: embed the ids from the request cookies.
import { checkoutAttributionFromCookies } from "flowgrid-sdk/server";
import { cookies } from "next/headers";
// Stripe
const session = await stripe.checkout.sessions.create({
// …
metadata: { ...checkoutAttributionFromCookies(await cookies()) },
});
// Lemon Squeezy
const checkout = await createCheckout(storeId, variantId, {
checkoutData: { custom: { ...checkoutAttributionFromCookies(await cookies()) } },
});
// (Creating the checkout client-side instead? Use fg.checkoutAttribution()
// from the browser SDK — same keys.)// LEG 2 — payments: Flowgrid reads the ids from the provider's webhook itself.
// Subscription events you send from your own webhook carry them explicitly:
import { attributionFromMetadata } from "flowgrid-sdk/server";
// Stripe customer.subscription.created — unwraps `.metadata` automatically
const { visitorId, sessionId } = attributionFromMetadata(event.data.object);
// Lemon Squeezy — unwraps `.meta.custom_data` automatically
// const { visitorId, sessionId } = attributionFromMetadata(payload);
await fg.trackSubscription({ event: "start", subscriptionId, userId, plan, mrr }, { visitorId, sessionId });With the real visitorId + sessionId on the payment, revenue joins the
visitor's full journey — the session, landing page and UTM campaign that
earned it — instead of a synthetic server visitor.
API + server-side error tracking — errors become error_events with
route/method/status context; FlowGrid-internal noise is filtered out:
try { … } catch (err) {
await fg.trackError(err, { route: "/api/teams/edit/[teamId]", method: "PATCH", statusCode: 500, userId });
throw err;
}
// Or wrap a handler — tracks and rethrows, framework behaviour unchanged:
export const PATCH = fg.withErrorTracking(handler, { route: "/api/teams/edit/[teamId]", method: "PATCH" });
// Opt-in process-level capture (Node only; observes, never swallows):
const detach = fg.captureUncaught();Like the browser transport, the server client never throws for delivery
problems — every method resolves { ok, status?, reason? }.
Cookie Consent
What you're asking consent for
When a visitor accepts cookies, this is exactly what each category permits FlowGrid to do (this is what the SDK enforces, not aspirational copy — each category maps to a hard gate in the transport).
Default posture: implied consent. Every category starts granted, and
tracking runs out of the box — the "declined" behaviour below only applies
when you've installed a consent gate (requireExplicitConsent: true or the
opt-in banner) and the visitor actively rejects a category:
| Category | Can the visitor decline? | What it covers |
| ------------- | ------------------------ | -------------- |
| necessary | No — always on | The visitor/session identifiers (visitor_id cookie, fg_session_id) and the consent cookies themselves (fg_consent, fg_tracking_consent). Required for the service to function; no behavioural profiling. |
| analytics | Yes | All event tracking. Page views, sessions, clicks, forms, searches, feature usage, funnels, performance/Web Vitals, engagement, identity events (identifyUser, active-user pings), and session replay recordings — plus the device/page context attached to each event (screen size, device type, language, URL, path, title, referrer). Declined → no events leave the browser. |
| marketing | Yes | Campaign attribution. The utm_source/medium/campaign/term/content and ref/via/source parameters read from the URL and the __flowgrid_* landing-page cookies, attached to events as _utm_*/_ref properties. Declined → events (if analytics is granted) are sent without any campaign attribution. |
| preferences | Yes | UI preferences your own app stores (locale, theme). FlowGrid itself stores nothing in this category — it exists so your banner can offer it. |
Independent of the banner, visitors sending Do-Not-Track or Global
Privacy Control are treated as having declined non-essential tracking by
default (respectDNT: true), and localhost traffic is never tracked unless
explicitly enabled.
Server-side events (flowgrid-sdk/server) are outside the browser consent
gates by design: they represent your backend's own records (auth events,
API feature usage, server errors) under your contractual/legitimate-interest
basis. If you want browser consent to extend to server calls, check your
stored consent state before calling the server client.
No-code (one tag) — recommended
Drop a single element on the page and FlowGrid renders a styled, opt-in, GDPR/CCPA-friendly consent banner that blocks analytics, marketing and session-replay tracking until the visitor accepts — with zero JavaScript:
<script src="https://cdn.flow-grid.xyz/flowgrid.min.js" data-site="YOUR_WEBSITE_ID"></script>
<div data-cookie-ccbanner></div>The banner auto-initializes ahead of (and independently of) tracking, so consent gates engage before the first event fires. It re-scans for late-mounted markers (page builders / SPA routes) and is idempotent.
Tier 1 — attributes (no CSS). Tune copy, colours, position via data-cc-*:
<div data-cookie-ccbanner
data-cc-position="bottom-right"
data-cc-primary="#EBF212"
data-cc-radius="16px"
data-cc-privacy-url="/privacy"></div>Common attributes: data-cc-mode (opt-in|opt-out), data-cc-title,
data-cc-description, data-cc-accept-label, data-cc-reject-label,
data-cc-position, data-cc-primary / -bg / -text, data-cc-radius,
data-cc-categories (analytics,marketing), data-cc-cookie-name / -days /
-domain, data-cc-respect-dnt, data-cc-theme (flowgrid|light|dark),
data-cc-manage-selector (re-open hook). Add data-cc-manual to a tag to skip
auto-init.
Tier 2 — your CSS. Add data-cc-unstyled to drop the default look and style
the stable class hooks (.fg-cc-banner, .fg-cc-card, .fg-cc-btn--accept, …)
and CSS variables (--fg-cc-bg, --fg-cc-primary, …) from your own stylesheet.
Tier 3 — bring your own markup. Put your own HTML inside the marker; FlowGrid binds your buttons by data-role and never injects DOM:
<div data-cookie-ccbanner style="display:none">
<h2>Cookies?</h2>
<button data-cc-accept>Sure</button>
<button data-cc-reject>No thanks</button>
</div>Global defaults can be set on window.FlowGridConfig.consentBanner before the
tag; per-element data-cc-* attributes override them. SPA apps can re-scan after
a client-side navigation with FlowGrid.wireConsentBanner() (or import
wireDeclarativeBanners() directly).
Programmatic banner
Render the same banner from code with renderConsentBanner():
import { renderConsentBanner } from "flowgrid-sdk";
const banner = renderConsentBanner({
mode: "opt-in",
title: "We value your privacy",
privacyPolicyUrl: "/privacy",
theme: { position: "bottom-right", primaryColor: "#EBF212" },
});
document.getElementById("manage-cookies")?.addEventListener("click", () => banner?.show());Core ConsentManager
The SDK ships a framework-agnostic ConsentManager. Render your own banner UI in whatever framework you use, and call into the manager to persist preferences.
import { ConsentManager, FlowGridTransport } from "flowgrid-sdk";
const consent = new ConsentManager({
cookieDomain: ".example.com",
cookieExpiry: 365,
respectDNT: true,
onChange: (prefs) => {
FlowGridTransport.setConsent({
analytics: prefs.analytics,
marketing: prefs.marketing,
});
},
});
consent.acceptAll();
consent.rejectNonEssential();
consent.update({ analytics: true, marketing: false, preferences: true });
consent.hasCategory("analytics"); // boolean
consent.reset();CookieBannerConfig and CookieBannerTheme types are exported from flowgrid-sdk/consent if you want a typed config object to pass to your own banner component.
You can also gate the transport directly without ConsentManager:
import { FlowGridTransport } from "flowgrid-sdk";
FlowGridTransport.setConsent({ analytics: true, marketing: false });
FlowGridTransport.hasConsent("analytics"); // truePackage Exports
| Import path | Description |
| -------------------------- | ------------------------------------------------ |
| flowgrid-sdk | Unified FlowGrid class + every feature module |
| flowgrid-sdk/analytics | Analytics modules only |
| flowgrid-sdk/core | Core modules (activation, experiments, feature flags, prompts) |
| flowgrid-sdk/react | useFlag, <Flag>, useExperiment, <Experiment> |
| flowgrid-sdk/next | getServerFlag/getServerFlags, getVariant (server-side) |
| flowgrid-sdk/consent | ConsentManager + types |
| flowgrid-sdk/server | FlowGridServer — Node/edge tracking (identity, feature usage, subscriptions, errors) + getServerFlags |
| flowgrid-sdk/types | Shared TypeScript type definitions |
Documentation
Full reference: https://flow-grid.xyz/documentation
License
MIT © Flowgrid
Contact Support [email protected]
