Oxwyn Studio
All field reports
23 July 2026Privacy

Consent-gated analytics that still tell you the truth

Most cookie banners are theatre bolted onto trackers that already fired. Ours is a gate, and the analytics behind it are better for it.

Open the network tab on most websites with a cookie banner and watch what happens before you click anything: the analytics tag is already loaded, the cookies are already set, and the banner is asking permission for something that happened seconds ago. That is not consent, it is decoration. When we built oxwynstudio.com we decided the banner would be a real gate: decline it and not one analytics byte leaves your browser.

The surprise was not that this is legally tidier. The surprise was that the analytics we kept turned out to be perfectly liveable, and in some ways more honest than the full-fat version. This is the pattern, the trade-offs, and what running a studio on smaller-but-truthful data actually looks like.

What "load nothing until consent" means technically

The phrase gets used loosely, so here is the strict version we implement. Before a visitor explicitly accepts:

  • The GA4 script (gtag.js) is not requested. Not loaded-and-paused, not loaded with storage disabled. The <script> tag does not exist in the document.
  • The Microsoft Clarity snippet (a session-recording and heatmap tool) is likewise absent.
  • Zero analytics cookies exist. No _ga, no Clarity identifiers, nothing.
  • No request of any kind goes to googletagmanager.com or clarity.ms. A visitor who declines and browses the whole site produces no traffic to either domain, ever.

The mechanism is deliberately boring: the analytics scripts are rendered conditionally by React, and the condition is the stored consent value. This is the shape of the real component in production:

type Consent = "granted" | "denied" | null

const COOKIE = "oxwyn-consent"
const OPEN_EVENT = "oxwyn:open-consent"

export function ConsentManager() {
  const [consent, setConsent] = useState<Consent>(null)
  const [visible, setVisible] = useState(false)

  useEffect(() => {
    const current = readConsent()       // parses document.cookie
    setConsent(current)
    setVisible(current === null)        // banner only when undecided
    const onOpen = () => setVisible(true)
    window.addEventListener(OPEN_EVENT, onOpen)
    return () => window.removeEventListener(OPEN_EVENT, onOpen)
  }, [])

  const decide = (value: "granted" | "denied") => {
    writeConsent(value)                 // one first-party cookie, one year
    setConsent(value)
    setVisible(false)
  }

  return (
    <>
      {consent === "granted" && gaId && (
        <Script src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
                strategy="afterInteractive" />
      )}
      {consent === "granted" && clarityId && (
        <Script id="clarity-init" strategy="afterInteractive">
          {/* Clarity bootstrap snippet */}
        </Script>
      )}
      {visible && <ConsentBanner onDecide={decide} />}
    </>
  )
}

If consent is not the string "granted", the Script components simply never render, and Next.js never injects them. There is no tag manager holding a queue of suppressed events, no half-initialised SDK. The scripts are absent the way a component behind a false condition is absent, which is the most reliable kind of absent that front-end engineering has to offer.

One first-party cookie does exist after a decision is made: oxwyn-consent, holding the word granted or denied for a year. Storing the refusal is what stops the banner reappearing on every page view, and remembering a "no" is about as privacy-friendly as a cookie gets.

A wall of dormant screens with a single standby light: what "load nothing until consent" actually looks like

Why default-deny is the legal position, and why that is fine

Under UK GDPR and PECR (the Privacy and Electronic Communications Regulations, the rules that govern storing or reading anything on a visitor's device), consent for non-essential cookies must be a freely given, informed, affirmative act. The ICO has been consistent on the practical meaning: analytics cookies are not "strictly necessary", pre-ticked boxes are not consent, and continuing to browse is not consent. A tag that fires before the banner is answered is a tag that fired without a legal basis. We are engineers, not lawyers, and none of this is legal advice, but the direction of travel is not subtle.

Most of the industry responds with consent-mode workarounds: load the tag, restrict it, upgrade it retroactively. We went the other way because default-deny is also just simpler to build and to audit. There is one condition in one component. Anyone can verify the behaviour in thirty seconds with devtools open: decline, browse, watch nothing happen. Try auditing a tag manager container with forty conditionally-triggered tags and a consent-mode integration, and simplicity starts to look like a compliance feature in its own right.

The honest objection is data loss, so let us take that seriously rather than wave at it.

What you lose, what you keep

What we lose. Every visitor who declines, plus every visitor who never interacts with the banner at all, is invisible to GA4 and Clarity. That is not a rounding error; depending on your audience it can be a large share of sessions, and a skewed share at that, since privacy-conscious visitors decline more. Session recordings, scroll heatmaps, multi-touch attribution and demographic reports all shrink to the consenting subset. Anyone who tells you this costs nothing is selling something.

What we keep, without any consent needed. Two sources, both cookieless:

  • Server and edge logs. Requests hitting the platform are visible in aggregate: which routes get traffic, response codes, referrer patterns. No client-side script, nothing stored on the device.
  • Vercel Analytics, which is mounted unconditionally precisely because it is cookieless by design. It counts page views and visitors in aggregate without setting identifiers on the device or tracking anyone across sites, which is why it sits outside the consent gate while GA4 sits behind it.

So the shape of the data is: complete, honest topline (every page view, every route, consent or no consent) from the cookieless layer, plus rich behavioural detail from the subset who opted in.

Designing decisions around smaller-but-honest data

Here is the reframing that made peace with the trade-off: consented analytics is a sample, and samples are fine if you treat them as samples.

Most of the decisions a studio site actually needs to make are topline questions. Which services pages earn traffic? Do readers arrive at our insights articles from search? Did the contact page get more visits after the redesign? Cookieless aggregate answers all of them with a full census, not a sample, because it sees every visitor.

The questions that need the consented layer are behavioural: where do people hesitate on the pricing page, does the Website as a Service comparison table get read or skipped, does the contact form's staged flow confuse anyone. For those, a consenting subset is genuinely useful as long as you resist false precision. We treat Clarity recordings as qualitative research (watch ten sessions, find three confusions) rather than quantitative truth, and we do not report conversion rates to two decimal places from a self-selected sample. Directional evidence, honestly labelled.

There is also a discipline benefit that we did not anticipate. When data is expensive, you stop hoarding it. Nobody adds a tracking pixel "just in case" when the pixel sits behind a gate most visitors never open. Every measurement has to answer a question someone is actually asking, which is a better analytics culture than the default of recording everything and understanding nothing.

Reviewing growth numbers together: smaller, honest data still supports real decisions

Consent you can take back

UK GDPR requires that withdrawing consent be as easy as giving it, and this is where most implementations quietly fail: the banner appears once, and after that the choice is buried or gone. Our answer is a small event-driven reopen pattern.

The banner component listens for a custom DOM event, and a plain button in the site footer dispatches it:

export function CookieSettingsButton() {
  return (
    <button type="button"
            onClick={() => window.dispatchEvent(new Event("oxwyn:open-consent"))}>
      Cookie settings
    </button>
  )
}

Because the footer renders on every page, the settings are always one click away. Click it, the banner returns, and a previous "accept" can become a "decline" (or the reverse) on the spot. The custom event keeps the footer and the consent manager decoupled: neither imports the other, and the banner logic stays in one file.

One honest limitation worth naming: flipping from granted to denied updates the stored cookie immediately, but scripts already executing in the current page session have already run. The next navigation renders with the gate closed and loads nothing. Removing an already-initialised third-party script from a live page is somewhere between unreliable and impossible, which is one more argument for never loading things optimistically in the first place.

The pattern travels

Nothing here is specific to our stack beyond the syntax. The ingredients are: a single stored consent value, scripts that are conditionally rendered rather than conditionally activated, a cookieless aggregate layer that works for everyone, and an always-available reopen control. It fits any framework that can render a script tag conditionally, which is all of them.

The deeper point is that "privacy-respecting" and "flying blind" are not the same thing, however often they are conflated. We know how many people visit, what they read and where they came from, for every visitor. We additionally know how a consenting subset behaves, and we weight that evidence accordingly. The banner tells the truth, the network tab agrees with it, and the numbers we plan around are numbers we can defend.

Key takeaways

  • "Load nothing until consent" should be literal: conditionally render the script tags themselves, so declined visitors trigger zero requests to analytics domains and zero cookies.
  • Default-deny matches the direction of UK GDPR and PECR (affirmative consent, no pre-ticked anything) and is dramatically easier to audit than consent-mode workarounds.
  • Keep a cookieless aggregate layer (server logs, Vercel Analytics) outside the gate: it gives you a complete topline census with nothing to consent to.
  • Treat consented analytics as a qualitative sample, not a census: good for finding confusion, wrong for two-decimal conversion claims.
  • Make consent revocable with an always-visible reopen control; a custom DOM event keeps the footer button and banner logic cleanly decoupled.

Frequently asked

Does declining cookies break anything on the site?

No. The site is built so that analytics are strictly additive: every page, form and feature works identically whether consent is granted, denied or never given. The only difference is whether GA4 and Clarity load in the background.

Is Google Consent Mode not enough?

Consent Mode adjusts how Google tags behave before and after consent, and in its advanced form still sends cookieless pings before any choice is made. Whether that satisfies UK regulators is a question for lawyers, and we are not lawyers. Our view as engineers is simpler: not loading the script at all is the only version whose behaviour you can verify completely from the network tab.

How do you measure anything if most visitors decline?

Cookieless sources cover the topline for every visitor: page views, routes, referrers, response codes, via server logs and Vercel's cookieless aggregate analytics. Consented GA4 and Clarity data then adds behavioural depth for the subset who opted in, which we treat as directional evidence rather than precise measurement.

Is the consent cookie itself a problem under PECR?

Storing the visitor's own consent choice is generally treated as strictly necessary, since without it the site cannot honour the decision and would have to re-ask on every page. Ours is a single first-party cookie containing the word granted or denied and nothing else, and it is documented in the cookie policy.

Put this to work

Want a system built like this behind your business?

Brief the studio