Custom Code: Head, Footer and Embeds

This content was generated with the assistance of AI and should be verified against the official Webflow documentation before being relied on in production.

This sub-section’s Webflow bibliography lists the reference material consulted while preparing these pages.

This page describes Webflow custom code as it was on 2026-10-28, the date on which its facts were checked against the official documentation. Custom code is the escape hatch of a visual builder: whatever the Designer cannot express, a site builder can add as HTML, CSS or JavaScript. Plans are named by tier only (Starter, Basic, Premium, Team, Enterprise), never by price; the Plans, Workspaces, Seats and Roles page explains which tier unlocks what.

The running example is Fernbrook Gardens, a fictitious botanical garden with English, Spanish and German locales. Every example below is something the Fernbrook team needs: a stylesheet override for the Plants collection cards, JSON-LD describing the garden as a visitor attraction, an analytics tag that must wait for visitor consent, and a small script that highlights the current bloom month. All identifiers, tokens and URLs in the examples are placeholders; replace them with your own values on a Developer workspace site before trying anything.

What custom code is and what it is not

Custom code in Webflow is client-side code. It is text that Webflow inserts into the generated HTML of your pages at publish time, so it runs in the visitor’s browser, on your domain, with the same privileges as any other script of your page. Three consequences follow, and the rest of this page is a consequence of one of them.

  • Nothing runs on a server. A Webflow page has no server-side execution. A script cannot read the CMS database, keep a secret or call an API with a private key. Server logic belongs in Webflow Cloud or in your own backend (see Webflow Cloud).

  • Webflow does not review it. The Designer shows you the code as text. It does not validate it, sandbox it or tell you that it breaks the layout. The site builder is responsible for what it does.

  • It is part of the published artifact. Custom code is published with the site, versioned only by the site’s backups, and exported with the site’s code (see Code Export and Portability).

For the HTML and CSS that the Designer itself produces, read HTML and CSS in Webflow. For attributes and the webflow.js runtime, read Custom Attributes and JavaScript. For loading external libraries safely, read Third-Party Libraries and Scripts.

The places where custom code can live

There are four places to put code through the product, and a fifth that is reached through the API.

Place Where you find it Scope Typical use

Site head code

Site settings, Custom code, Head code

Every page of the site

Meta tags, global stylesheets and variables, analytics and consent loaders, JSON-LD for the organization

Site footer code

Site settings, Custom code, Footer code

Every page of the site, just before the closing body tag

Scripts that need the DOM, chat widgets, site-wide behaviour

Page head and footer code

Page settings, Custom code, Inside head tag and Before body tag

One page (or one CMS template, which makes it one per item)

Page-specific styles and scripts, structured data for that page

Code Embed element

Add panel, Components section, Embed, then the code editor

The place where the element sits on the page

Widgets, inline SVG, a style block for one section, a map or booking widget

Registered scripts through the Data API

Created with the API and applied to a site or page

Whatever you apply them to

Distributing the same script to many sites without pasting it

The first four are the subject of this page; the last is described in Data API: Assets, Forms and Custom Code.

The difference is when the browser meets the code, and the choice matters more than it looks.

  • Head code is parsed before the page body exists. A <style> or <link rel="stylesheet"> here applies before the first paint, which avoids a flash of unstyled content. A <script> here blocks parsing unless it is marked defer or async, and it cannot find elements that have not been parsed yet.

  • Footer code (the "before body tag" box) is parsed after the markup and, in the generated page, near the end of the body. Scripts here can reach the DOM directly, and they do not delay the first paint.

A rule of thumb that works for Fernbrook: styles, structured data, consent defaults and tag loaders go in the head; behaviour that reads or changes elements goes in the footer or, better, waits for readiness (see below).

flowchart TD A["Visitor requests /visit"] --> B["Browser parses head"] B --> C["Site head code, then page head code"] C --> D["Browser parses body markup, including Code Embeds in place"] D --> E["webflow.js runtime and site footer code"] E --> F["Page footer code"] F --> G["DOMContentLoaded and load events"] G --> H["wf.ready callbacks run"]

The diagram is a simplified view: the order of the two footer boxes and of the runtime script is decided by the generated page, so a script must not depend on running before or after another block of custom code. If it needs a guarantee, it should wait for an event or for wf.ready().

Site code versus page code

Site-level code is published on every page, including the 404 page and every CMS template page. Page-level code is published only on that page. For a CMS template page, the page-level code is the same for every item, so any item-specific value has to come from a Code Embed that holds a CMS field (see CMS fields in embeds).

Keep the site-level boxes small. Everything in them is downloaded by every visitor on every page, so a Fernbrook script that only the Garden Journal needs belongs on the Journal template page, not in the site footer.

The Code Embed element

A Code Embed is an element that holds a block of HTML, CSS or JavaScript, which Webflow outputs verbatim at the position of the element in the page. It is the right tool when the code has a place on the page: a map widget inside a section, an inline SVG icon, or a <style> block that belongs to one component.

<!-- Code Embed inside the "Visit" page hero -->
<div class="fg-opening-hours" role="note">
  <strong>Open daily</strong>
  <span>Sunrise to dusk</span>
</div>
<style>
  .fg-opening-hours {
    display: inline-flex;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border-radius: 999px;
    background: #e8f1e4;
    color: #1d3a24;
  }
</style>

Things to know about the element.

  • It is a black box on the canvas. The Designer shows a placeholder or, for some content, a rendering. The Navigator lists it as an Embed. You cannot style the contents with the Style panel; you can style the Embed element itself, for example its margin and its position in a grid.

  • Its content is validated only a little. Webflow warns about some invalid markup when you save the embed, but it does not guarantee that the output is valid HTML. A <div> that is never closed will swallow the rest of the page.

  • It is not rendered per breakpoint. The same code appears at every breakpoint. Responsive behaviour has to be in your own CSS (media queries).

  • It does not know about Webflow classes you did not write. Webflow class names can change when a designer renames them, and some elements get generated w- classes; do not target generated names from a Code Embed.

  • A script in an embed runs where the embed is. Inside a component, it runs once per instance of the component on the page, so it must be written to cope with several copies (see Components, Props, Slots and Variants).

Situation Prefer Reason

A style rule that restyles an element Webflow generates, such as a form success message

Site or page head code

It must exist for the whole page and be parsed early

A third-party widget that mounts into a container

Code Embed placed in the container

The markup and the script stay together with the place on the page

A tag that must run on every page

Site head or footer code

One place to maintain, no copies

JSON-LD for one specific page

Page head code, or a Code Embed on that page

It is page-specific; both work

JSON-LD that comes from CMS fields

Code Embed on the template page

Only an embed can hold a CMS field

Limits and plan requirements

Character limits

On 2026-10-28 the Webflow update that raised the limit, together with the Help Center article on head and body code, give 50,000 characters for each of the site head box, the site footer box, the page head box, the page footer box and each Code Embed. Older articles, books and forum threads still quote 10,000 characters for the site boxes and 20,000 characters for the page boxes, or even lower figures for embeds. Those numbers are outdated; the old limits were raised, and a script that was refused years ago may now fit.

The limit is on the characters of the source you paste, not on the bytes that visitors download. Webflow minifies the JavaScript and CSS that it generates for the site, but the contents of custom code are inserted as written. If a script exceeds the limit, host it as a file and reference it with a <script src> tag (see Third-Party Libraries and Scripts), or register it through the Data API.

Plan requirements

Custom code is not available on every plan, and the official pages describe the requirement in two ways. On 2026-10-28 the Help Center article on custom code in head and body tags speaks of a paid Workspace plan (a plan of the Workspace that owns the site), while other Help Center pages describe custom code as a feature of a paid Site plan (the plan that hosts the site on a custom domain). Both statements describe the same intent from different angles:

  • On a free Starter Workspace a site cannot use custom code, apart from the free Developer workspace described in the next bullet.

  • The Developer workspace is free and allows custom code on sites that are not hosted, which makes it the recommended sandbox for every example on this page. It cannot publish to production.

  • To run custom code on a hosted site on a custom domain, the Workspace needs a paid plan and the site needs a paid Site plan (or the Team or Enterprise plans, which bundle both).

If you are unsure, test it: add a visible change to the site head code and publish to the staging domain (<site>.webflow.io). If the change does not appear on the published page, the plan is the first thing to check.

Who can edit custom code

Editing custom code requires a Designer seat with permission to change site settings. A Content editor (the Editor role that works on the canvas) cannot change it, and neither can a Site guest. This is a deliberate safety boundary: the person who can paste a script can change what every visitor sees and sends. Roles are covered in Plans, Workspaces, Seats and Roles.

Where custom code runs and where it does not

This is the most common source of confusion: custom code does not run while you are designing.

Surface Does custom code run? Notes

Designer canvas (editing)

No

Head code, footer code and Code Embed scripts are not executed. The canvas shows what the Designer can represent.

Preview mode

Only when enabled

Custom code is off by default in Preview and is enabled with a toggle at the bottom left of the canvas. When it is on, the page is served from a *.canvas.webflow.com address.

Comment mode

Rendered on the canvas domain

Comments are anchored to the canvas host, so a content security policy or domain check can affect it.

Staging domain (<site>.webflow.io)

Yes, after publishing

The closest match to production, and the place to test tags that check the hostname.

Custom domain (production)

Yes, after publishing

The domain that your visitors use; consent, analytics and domain restrictions are evaluated here.

Exported code

Yes, as plain files

Custom code is exported as part of the HTML; see code export.

flowchart LR subgraph Designing["Designing"] C["Designer canvas: no custom code"] P["Preview with custom code enabled: canvas.webflow.com"] end subgraph Publishing["Published"] S["Staging: site.webflow.io"] L["Live: custom domain"] end C --> P --> S --> L X["Exported static files"] L -.-> X

Preview on the canvas domain

When you enable custom code in Preview, the page is not shown from webflow.com or from your own domain: it is served from a host such as <short-name>.canvas.webflow.com. The Webflow developer documentation on custom code preview mode describes the same behaviour for apps and extensions. The implications are practical.

  • Domain allow lists. If a third-party service (an analytics property, a map key, a booking widget, a consent platform) accepts requests only from certain hostnames, the canvas host is not on the list. Preview will then show an error from that service, and the same code will work after publishing. Add the canvas host to the allow list only if the provider supports wildcards, and prefer testing on the staging domain.

  • Content security policy. If your own security headers (see Security) or those of a third party restrict where a page may be framed or where scripts may load from, the Preview host may be blocked. The Designer loads the canvas in a frame, so a policy that forbids framing makes Preview fail even when the site itself is fine.

  • Cookies and consent. A consent cookie set by a consent platform is scoped to the host that set it. The canvas host is a different host, so consent state does not carry over from the live site, and your banner will appear again.

  • Hostname checks in code. A script that does something like if (location.hostname === 'www.fernbrook.example') behaves differently on the canvas host and the staging domain. Write such checks so that a test environment is handled explicitly.

The advice that follows from all of this is simple: use Preview for quick visual checks of custom code, and use the staging domain for anything that depends on a hostname, a cookie or a header.

Writing CSS in custom code

CSS is the safest kind of custom code and the one the Designer cannot always replace. Typical needs are things the Style panel does not offer: :has() selectors, clip-path shapes, @container queries, @supports, ::selection, scrollbar styling, print styles, and overriding a style that Webflow generates.

A style block in the head

<style>
  /* Fernbrook: tokens that the Webflow Variables panel does not cover */
  :root {
    --fg-focus-ring: 3px solid #0b6b3a;
    --fg-radius-card: 0.75rem;
  }

  /* Visible focus for keyboard users on every interactive element */
  a:focus-visible,
  button:focus-visible,
  input:focus-visible {
    outline: var(--fg-focus-ring);
    outline-offset: 2px;
  }

  /* Respect the visitor's motion preference */
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      transition-duration: 0.01ms !important;
    }
  }
</style>

Overriding what the Designer generates

Webflow writes its own stylesheet, and your custom CSS is added to the page as well. Because the cascade decides, the position of your rules matters. Custom code in the head is emitted after the link to the Webflow stylesheet, so for rules of equal specificity yours win. Use a class that you add in the Designer rather than a deep selector.

/* Plants collection card: lift on hover, only where hover exists */
@media (hover: hover) {
  .plant-card {
    transition: transform 150ms ease, box-shadow 150ms ease;
  }

  .plant-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
  }
}

/* Highlight a card that is flowering this month, using a data attribute set from a CMS field */
.plant-card[data-flowering="now"] {
  border-color: #c2185b;
}

Guidelines that keep embedded CSS from becoming a maintenance problem:

  • Scope it. Prefix your own classes (fg- for Fernbrook), so they do not collide with generated classes such as w-button or w-form-done.

  • Do not fight the cascade with !important. Raise specificity with a combo class or move the rule to a Webflow class. The exception is the reduced-motion rule above, which is meant to win over everything.

  • Prefer variables. Define colours and spacing as Webflow Variables, and read them in CSS with the generated custom property name (see Variables and Modes). A hard-coded colour in an embed is invisible to the Variables panel and will not follow a mode switch.

  • Keep breakpoints consistent. Webflow’s breakpoints are max-width media queries; if you write your own, use the same boundaries (see Responsive Design and Breakpoints).

  • Remember that the canvas ignores it. A rule in an embed shows up in Preview with custom code enabled and on the published site, not while editing. Tell designers on the team, or they will try to "fix" a style that works.

Further reading on the language itself is in HTML and CSS.

Writing JavaScript in custom code

Waiting for the page

A script in the head runs before the elements exist, and a script in a footer may run before the Webflow runtime has set up interactions. The robust pattern is to wait.

// Plain DOM: wait for the markup
document.addEventListener('DOMContentLoaded', function () {
  document.documentElement.classList.add('js');
});

// Webflow runtime: wait until the Webflow scripts are ready
window.wf = window.wf || {};
// The browser API defines wf.ready(); use it when the code needs Webflow features.
if (typeof wf.ready === 'function') {
  wf.ready(function () {
    // Safe to use Webflow's browser API here
  });
}

wf.ready() belongs to the Webflow browser API and runs its callback when the Webflow runtime is ready. It is documented in the developer documentation, and the consent functions of the same API are shown later on this page. The older calls such as Webflow.push(), Webflow.destroy() or Webflow.require('ix2') are community-documented rather than official, and do not apply to Interactions with GSAP; they are discussed, with that label, on Custom Attributes and JavaScript.

A small behavioural script for Fernbrook

The Plants page marks cards for plants that bloom this month. The bloom months come from a CMS field that the template writes into a data attribute (see CMS fields in embeds); the script only reads it.

(function () {
  'use strict';

  var month = new Date().getMonth() + 1; // 1 to 12

  function markFlowering() {
    var cards = document.querySelectorAll('.plant-card[data-bloom-months]');
    cards.forEach(function (card) {
      var months = card.getAttribute('data-bloom-months')
        .split(',')
        .map(function (m) { return parseInt(m, 10); });
      if (months.indexOf(month) !== -1) {
        card.setAttribute('data-flowering', 'now');
      }
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', markFlowering);
  } else {
    markFlowering();
  }
})();

The script is wrapped in an immediately invoked function so that its variables do not leak into the global scope, where another script of the page (or Webflow itself) could collide with them. It does not use jQuery. Webflow’s runtime includes a bundled jQuery on published sites, but depending on it from custom code ties the script to an implementation detail; plain DOM APIs are enough here. If you want to understand what that bundled library offers, see jQuery.

Use deferred external files for large scripts

When a script outgrows the limit or is shared between sites, load it as a file.

<script src="https://cdn.fernbrook.example/js/bloom-filter.1.2.0.js"
        defer
        integrity="sha384-PLACEHOLDER_HASH_REPLACE_ME"
        crossorigin="anonymous"></script>

Pin a version in the file name, add a subresource integrity hash, and use defer so that the script does not block parsing. The details, and the performance and privacy review that should accompany a third-party script, are in Third-Party Libraries and Scripts.

Structured data (JSON-LD)

Search engines and answer engines read structured data to understand what a page is. JSON-LD is the format that Google and others recommend, and it is a <script type="application/ld+json"> block, which is custom code. It is inert: it is data, not executed code, so it is safe in the head.

Site-wide organization data

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Fernbrook Gardens",
  "url": "https://www.fernbrook.example",
  "logo": "https://www.fernbrook.example/logo.png",
  "sameAs": [
    "https://www.example.org/fernbrook-gardens"
  ]
}
</script>

Per-item data from CMS fields

A single block in the site head cannot describe a Plants item, because it has no access to the item. Put the block in a Code Embed on the template page and insert fields with the Add field control of the embed editor, which writes a token for the field into the code. At publish time Webflow replaces each token with the item’s value.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "{{Name}}",
  "description": "{{Summary}}",
  "image": "{{Main image URL}}",
  "datePublished": "{{Publish date}}",
  "author": { "@type": "Organization", "name": "Fernbrook Gardens" }
}
</script>

The double braces above stand for the field tokens that the embed editor inserts; they are not something you type freely, and the exact appearance in the editor is a chip rather than text. Mind the JSON syntax: a field that contains a double quote or a line break will break the JSON, and no editor warns about it. For text fields that may contain such characters, prefer a plain-text field that editors keep short, or generate the data in your own build step. The mechanics, and the types of field that can be used, are explained in Conditional Visibility and Dynamic Embeds.

Validate the result with a structured-data testing tool of your search engine, and see SEO and AEO for what structured data is for and which types matter.

Analytics and marketing tags

Analytics and advertising tags are the most common custom code on production sites, and the most common source of privacy problems.

A tag manager or analytics loader in the head

<!-- Fernbrook analytics loader: placeholder ID, loaded only after consent (see below) -->
<script>
  window.fgAnalyticsId = 'G-PLACEHOLDER';
</script>

The snippet above only stores an identifier. The loader that uses it appears in the next section; keeping the two separate makes it possible to load the tag conditionally.

Practical points for tags:

  • Prefer one loader. A tag manager lets marketing change tags without touching Webflow, but it also lets anyone with access to it add scripts to your domain; treat access to the tag manager as access to your site.

  • Publish after every change. A tag added in the head code appears on the live site only after you publish.

  • Test on staging. Most analytics tools filter or reject the canvas and staging hosts, which is useful: the numbers of the live property are not polluted by testing.

  • Webflow Analyze. Webflow’s own analytics (see Analyze and Optimize) does not need a tag from you, and combining it with a third-party tag is a decision about consent, not just about data.

Under the GDPR and the ePrivacy rules that apply to Fernbrook’s European visitors, analytics and advertising cookies and similar technologies need consent before they are set. Loading the tag first and asking afterwards is the usual mistake; the correct order is to ask, remember the answer, and load the tag only when the answer allows it.

How the flow works

sequenceDiagram participant V as Visitor participant B as Banner and consent platform participant S as Site head code participant T as Analytics tag V->>S: Opens a Fernbrook page S->>B: Reads the stored choice alt choice already stored as allow B->>T: Load the tag else no choice or deny B->>V: Shows the banner or stays silent V->>B: Chooses allow B->>T: Load the tag end

The example below stores the choice in localStorage under a Fernbrook key, loads the analytics tag only after the visitor allows it, and also tells Webflow’s own tracking about the choice through the browser API.

<script>
(function () {
  'use strict';

  var KEY = 'fg-consent-analytics';

  function loadAnalytics() {
    if (document.getElementById('fg-analytics')) { return; }
    var s = document.createElement('script');
    s.id = 'fg-analytics';
    s.async = true;
    s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(window.fgAnalyticsId);
    document.head.appendChild(s);
    window.dataLayer = window.dataLayer || [];
    window.gtag = function () { window.dataLayer.push(arguments); };
    window.gtag('js', new Date());
    window.gtag('config', window.fgAnalyticsId, { anonymize_ip: true });
  }

  function applyChoice(choice) {
    try { localStorage.setItem(KEY, choice); } catch (e) { /* storage blocked */ }
    if (typeof wf !== 'undefined' && typeof wf.ready === 'function') {
      wf.ready(function () {
        if (choice === 'allow') { wf.allowUserTracking(); }
        else { wf.denyUserTracking(); }
      });
    }
    if (choice === 'allow') { loadAnalytics(); }
  }

  var stored = null;
  try { stored = localStorage.getItem(KEY); } catch (e) { /* storage blocked */ }
  if (stored === 'allow' || stored === 'deny') { applyChoice(stored); }

  // The banner buttons call these functions
  window.fgConsent = {
    allow: function () { applyChoice('allow'); },
    deny: function () { applyChoice('deny'); }
  };
})();
</script>

The Webflow browser API offers three functions on the global wf object, which are called inside wf.ready(): wf.getUserTrackingChoice() returns the visitor’s current choice (the documented values are allow and deny), wf.allowUserTracking() opts the visitor into Webflow’s tracking and wf.denyUserTracking() opts out. They govern Webflow’s own tracking features (for example Analyze); they do not control third-party tags, which is why the example also gates the analytics tag itself. To check the stored state in a browser, open the developer tools console and run wf.getUserTrackingChoice().

The banner

The banner is ordinary Designer markup (a fixed div with two buttons) whose buttons call the functions above. It is simple to build in the Designer, and its custom attribute hooks are described in Custom Attributes and JavaScript. Consent platforms provide a ready-made banner and a stored choice; whichever you use, the rule is that nothing that sets a cookie or sends personal data runs before the choice is known.

<div class="fg-consent" role="dialog" aria-label="Cookie preferences" hidden>
  <p>Fernbrook Gardens uses analytics cookies to improve this website.</p>
  <button type="button" onclick="fgConsent.allow(); this.parentNode.hidden = true;">Allow</button>
  <button type="button" onclick="fgConsent.deny(); this.parentNode.hidden = true;">Decline</button>
</div>

For the Spanish and German locales the banner text has to be localized as well. A Code Embed is part of the page content of its locale only if the embed is added to the localized page, so check the locale variants of the page that holds the banner (see Localization). A compliance decision, such as whether "decline" is as prominent as "allow", is for the site owner and counsel, not for this page.

Other embeds that set cookies

The same rule applies to video embeds, maps, chat widgets and social buttons, which set cookies or send visitor data to their providers as soon as they load. Load them on demand: show a placeholder with a button, and create the iframe when the visitor asks for it.

document.querySelectorAll('[data-fg-embed-src]').forEach(function (box) {
  box.querySelector('button').addEventListener('click', function () {
    var frame = document.createElement('iframe');
    frame.src = box.getAttribute('data-fg-embed-src');
    frame.title = box.getAttribute('data-fg-embed-title');
    frame.loading = 'lazy';
    frame.referrerPolicy = 'no-referrer';
    box.replaceChildren(frame);
  });
});

CMS fields in embeds

A Code Embed placed on a Collection template page, or inside a Collection list item, can contain field tokens. At publish time Webflow substitutes each token with the value of the field of that item, so the embed turns into item-specific markup. This is how the Fernbrook JSON-LD above is generated, how a data attribute that holds the bloom months is written, and how a per-item map or video is configured.

<!-- Inside the Plants collection list item -->
<article class="plant-card"
         data-bloom-months="{{Bloom months}}"
         data-plant-slug="{{Slug}}">
  <h3>{{Name}}</h3>
</article>

Rules that follow from how substitution works:

  • The tokens are replaced as text, with no escaping. A value containing a quotation mark, an angle bracket or a script fragment is inserted as is. That is both a correctness risk and a security risk; see Security.

  • Only fields of the current item (or of the Collection list the embed sits in) are available. Fields of a referenced item are available through the reference field on the page, but not inside a head code box.

  • The page-level head and footer boxes of a template page do not accept field tokens in every case, so for item-specific head content use a Code Embed on the page. Conditional rendering and the list of field types that work are covered in Conditional Visibility and Dynamic Embeds.

  • Every published item produces its own HTML, so a large embed multiplies in size across thousands of items. Keep embeds on template pages small.

Registered scripts and the Data API

For agencies and for apps, pasting a script by hand into dozens of sites is slow and error-prone. The Data API lets an app register a script (a hosted file with an integrity hash, or an inline snippet) and then apply it to a site or to a page, with the version and the location (header or footer) as parameters. The registered script appears in the site’s custom code panel, and the Designer user can see it but not edit the code the app manages.

The relevant points for this page are:

  • Registered scripts are intended for apps and for distribution to many sites, not for the first hand-written snippet of a Fernbrook developer.

  • They require an authorization scope for custom code (see Data API Authentication and Scopes).

  • They are described, with request examples, in Data API: Assets, Forms and Custom Code.

  • A script that is applied through the API still counts against the same plan requirements and is still published only when the site is published.

Security: what custom code can do

Any script that runs on your page can do everything your own page can do. That includes reading and changing the document, reading the cookies that are not marked HttpOnly, reading localStorage, listening to every key press and form field, sending requests to any server that allows it, and loading further scripts. There is no sandbox between custom code and the rest of the page.

Capability of a script on your domain Consequence Mitigation

Read form fields, including a password or a payment field

Data theft by a compromised third-party script

Do not load untrusted scripts on pages with sensitive forms; use hosted payment fields that live in an iframe of the provider

Change the page

Defacement, phishing overlays, fake login forms

Review every script, pin versions, use subresource integrity on files

Load further scripts

A trusted script that is later compromised brings in new code

Prefer self-hosted copies of a pinned version; restrict sources with a content security policy

Read non-HttpOnly cookies and storage

Session or identifier leak

Keep secrets out of cookies that script can read

Call APIs from the visitor’s browser

Any key in the page is public

Never place a secret, a site token or an API key in custom code; use a public key meant for browsers, or a server

Practical rules for the Fernbrook team:

  1. Treat access like deploy rights. Whoever can edit custom code, or the tag manager that feeds it, can change what visitors run. Grant the Designer role sparingly and remove it when people leave.

  2. Never put credentials in custom code. Everything in a head, footer or embed is delivered to every visitor and is visible in the page source. Use placeholders in documentation and keep tokens of the Data API on a server.

  3. Escape what you insert. A CMS field or a query string value that is written into markup without escaping can inject script. Use textContent instead of innerHTML in your scripts, and avoid putting free-text CMS fields inside inline script blocks.

  4. Pin and verify external files. Use a version in the URL and integrity with crossorigin so a changed file is refused by the browser.

  5. Restrict with headers where you can. Enterprise sites can add custom security headers such as a Content Security Policy, and an inline-script-heavy site will need nonce or hash support in the policy. A policy has to allow the Webflow hosts and the preview host, or the canvas and Preview stop working.

  6. Test the forms and checkout after every script change. A script that breaks a form silently loses leads.

Content Security Policy and custom code

A strict policy blocks inline scripts and styles unless they carry a nonce or hash, and Webflow publishes custom code inline. The result is that a site cannot adopt a strict policy without changing how its custom code is delivered (for instance, by moving code into hosted files that the policy allows). If you add a policy, apply it first in report mode, read the reports, and only then enforce it. Allow the hosts you actually use: the Webflow CDN, the staging domain, the preview host *.canvas.webflow.com if Preview is part of your workflow, and each third party.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://cdn.prod.website-files.com https://cdn.fernbrook.example;
  img-src 'self' data: https://cdn.prod.website-files.com;
  frame-ancestors 'self' https://*.webflow.com

The header values above are a starting point for reporting, not a recommendation to enforce them: a site with inline custom code, Webflow’s own scripts and a consent platform will produce violations until the policy is tuned.

Performance considerations

Custom code is also the easiest way to lose a good performance score.

  • Every script has a cost. Parse, compile and execute time add up on a phone. Remove tags nobody reads.

  • Order matters. Put render-blocking CSS in the head, and everything else after the content or with defer.

  • Do not load libraries on every page when one page needs them. Page-level boxes exist for that reason.

  • Avoid layout shift. An embed that injects a banner above the content after load pushes everything down. Reserve space with CSS.

  • Lazy-load below the fold. Maps, videos and chat widgets should wait until they are near the viewport or until the visitor asks.

The broader checklist, including how Webflow minifies its own JavaScript and CSS on publish, is in Performance Optimization. The update on minified JavaScript by default confirms that Webflow’s generated scripts are minified; your pasted code is not, so keep it lean yourself.

Accessibility of custom code

Custom code can create or fix accessibility problems that the Designer cannot see. Webflow’s accessibility checklist lists custom code as a place to check: keyboard focus for custom widgets, names and roles for anything you build with a div, announcements for dynamic changes, and not removing focus outlines. The focus and reduced-motion styles shown earlier on this page are examples. A full checklist is on Accessibility.

Common pitfalls

Pitfall What to do instead

Expecting the script to run in the Designer

Open Preview and enable custom code, or publish to the staging domain

Pasting a snippet into the footer on every page when one page needs it

Use the page-level box or a Code Embed on that page

Relying on a quoted 10,000 or 20,000 character limit from an older article

The current limit is 50,000 characters per box and per embed; check the update before splitting a script

Forgetting to publish

Custom code reaches visitors only on publish; staged edits are invisible to the live site

Duplicate tags (once in the head, once in a tag manager)

Count events in the analytics tool and keep one loader

A CMS value that breaks JSON-LD or a script

Keep such fields short and plain, validate the output, avoid inserting free text into inline script

Loading analytics before consent

Gate the tag as shown above, and confirm that nothing sets a cookie before the choice

Using Webflow.push or ix2 calls for GSAP interactions

They do not apply to Interactions with GSAP; use GSAP’s own API, as in Interactions with GSAP

Putting a secret in a script because it is "only on our site"

Everything on the page is public; move the secret to a server

Testing on the canvas host and trusting the result for production

Test hostnames, cookies and headers on the staging domain

Editing code inside a component instance and expecting a global effect

Put shared behaviour in the head or footer and keep the embed to markup

Corrections to older tutorials

The book Webflow by Example (Packt, 2021) says nothing about custom code, and older Webflow tutorials describe a different state of the product: a 10,000 character limit for site code and 20,000 for page code, a Designer where custom code was a page-settings tab only, jQuery presented as the way to script a Webflow page, and the Preview host without the canvas domain. Where an older source and the Help Center disagree, follow the Help Center. The complete list of outdated claims is in Where Older Books and Tutorials Are Outdated.

Verifying what you published

A short checklist after any custom-code change makes failures visible before visitors find them.

  1. Publish to the staging domain and open the page in a private window.

  2. Open the browser developer tools. The Console should show no errors from your code, and the Network panel should show the files you expect, and no request to a tracking host before consent.

  3. View the page source and search for your snippet to confirm where it landed, in the head or the body.

  4. For JSON-LD, run the page through a structured-data testing tool of your search engine.

  5. For a CMS template, open two items, one with all fields filled and one with empty optional fields, and check that neither breaks the page.

  6. Repeat in the Spanish and German locales, which have their own pages and may carry different banner text.

  7. Only then publish to the custom domain and repeat a quick check there.

// Run in the browser console on the published page to see what was inserted
console.log(document.querySelectorAll('script[type="application/ld+json"]').length + ' JSON-LD block(s)');
console.log(Array.from(document.scripts).map(function (s) { return s.src || '[inline]'; }));

Where to go next