---
title: Do Not Sleep on the Custom HTML Module
description: The Custom HTML module is the sleeper feature of every SanityPress site. Pair with Claude and the Sanity MCP; no coding knowledge required.
---

# Do Not Sleep on the Custom HTML Module

![man completely unfazed and asleep on the couch, a large orange jellyfish with black eyes wearing a chef's hat, floating in the kitchen in the background](https://cdn.sanity.io/images/cyu7k2r0/production/2967fd36d6ba472ecf144ef166b9ae21640f48df-1344x896.png)

Every SanityPress site ships with a module most people scroll right past in the Studio. Two fields. An HTML field and a CSS field. Easy to overlook. A mistake to.

That simplicity is the whole point and the reason it's the most underrated module in the stack.

The **Custom HTML module** is a blank canvas, and a blank canvas is one of the most powerful things you can hand to an AI.

Here is the shift worth internalizing. You no longer need to know how to code. You just need to be able to describe what you want. If you can picture it on a webpage, you can ship it by asking Claude to write it straight into any module via the Sanity MCP. Every site running SanityPress has this capability sitting idle. Most never touch it.

## It’s a blank canvas

The module takes two things: HTML for structure and an optional CSS field for **scoped styling**. Drop in markup (and/or scripts) and you get static content. Add CSS and it gets styled. As the block comes alive, your JavaScript gets executed right there on the page. ([more on scoped CSS per module](/blog/scoped-css-per-module))

Here’s a couple of best practices tips:

**Tip 1:**  Use `<script defer>…</script>` so your scripts run safely after page load, without blocking any other critical scripts.

**Tip 2:**  Use IIFEs ("immediately invoked function expressions"; Claude will know) so variable and function names don’t collide with other scripts. i.e. this scopes everything within your module.

```html
<script defer>
  (function() {
    // code goes here...
  })()
</script>
```

Because each block carries its own styles and logic, you can go as wild as you want without it bleeding into the rest of the page. Animations, gradients, canvas effects, embeds, charts, calculators—all of it living in one self-contained `custom-html` block.

## You’ve been looking at it this whole time

You don't have to take my word for it. Scroll to the very bottom of this page. That field of drifting pixels that scatters when you drag across it is a Callout module with an embedded Custom HTML block—a single canvas element and a script that reads the site's own color tokens. No bespoke component. No custom code committed to the repo. I described it, and Claude built it.

Still not convinced? Here's the range this thing has:

- **[this Hero (cover) module demo runs an interactive ascii dither background](/modules/hero-cover)**
- **[this Custom HTML module page covers everything from inline scripts to third-party embeds.](/modules/custom-html)**

Same module. Wildly different results. Every single one driven by a prompt.

The only limit is what you can describe. So the real question is… *what will you build?*

## Imagine what you can ship

```html
<div id='sp-imagine-stage'>
  <canvas id='sp-imagine-canvas'></canvas>
  <div id='sp-imagine'>
    <span class='sp-imagine-prefix'>Imagine </span>
    <span class='sp-imagine-type'></span>
    <span class='sp-imagine-caret' aria-hidden='true'></span>
  </div>
</div>

<script>
(function() {
  var root = document.getElementById('sp-imagine');
  if (!root) return;
  var out = root.querySelector('.sp-imagine-type');
  if (!out) return;

  var phrases = [
    'a pricing calculator.',
    'an interactive product tour.',
    'a live data dashboard.',
    'a particle-powered hero.',
    'anything you can picture.'
  ];

  if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
    out.textContent = phrases[0];
    return;
  }

  var p = 0, i = 0, del = false;

  (function tick() {
    if (!document.body.contains(root)) return;

    var w = phrases[p];
    out.textContent = w.slice(0, i);

    if (!del) {
      if (i < w.length) {
        i++;
        setTimeout(tick, 55);
      } else {
        del = true;
        setTimeout(tick, 1400);
      }
    } else {
      if (i > 0) {
        i--;
        setTimeout(tick, 28);
      } else {
        del = false;
        p = (p + 1) % phrases.length;
        setTimeout(tick, 250);
      }
    }
  })();
})();
</script>

<script>
(function() {
  var canvas = document.getElementById('sp-imagine-canvas');
  if (!canvas) return;
  var stage = canvas.closest('#sp-imagine-stage');
  if (!stage) return;

  var ctx = canvas.getContext('2d');
  var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  var P = [];
  var pointer = null, raf = 0, dpr = 1, fg = '#000';

  function build() {
    var r = stage.getBoundingClientRect();
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    var W = canvas.width = Math.max(1, Math.round(r.width * dpr));
    var H = canvas.height = Math.max(1, Math.round(r.height * dpr));

    fg = getComputedStyle(stage).getPropertyValue('--color-foreground').trim() || '#000';

    var gap = Math.max(6, Math.round(9 * dpr));
    P = [];

    for (var y = gap / 2; y < H; y += gap) {
      for (var x = gap / 2; x < W; x += gap) {
        P.push({ hx: x, hy: y, x: x, y: y, vx: 0, vy: 0 });
      }
    }
  }

  function dot() {
    return Math.max(1, Math.round(1.2 * dpr));
  }

  function frame() {
    if (!canvas.isConnected) {
      cancelAnimationFrame(raf);
      clean();
      return;
    }

    var W = canvas.width, H = canvas.height, d = dot();
    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = fg;

    var R = 80 * dpr, R2 = R * R;

    for (var k = 0; k < P.length; k++) {
      var pt = P[k];

      if (pointer) {
        var dx = pt.x - pointer.x;
        var dy = pt.y - pointer.y;
        var dd = dx * dx + dy * dy;

        if (dd < R2 && dd > 0.01) {
          var di = Math.sqrt(dd);
          var f = (1 - di / R) * 5 * dpr;
          pt.vx += (dx / di) * f;
          pt.vy += (dy / di) * f;
        }
      }

      pt.vx += (pt.hx - pt.x) * 0.08;
      pt.vy += (pt.hy - pt.y) * 0.08;
      pt.vx *= 0.82;
      pt.vy *= 0.82;
      pt.x += pt.vx;
      pt.y += pt.vy;

      ctx.fillRect(pt.x, pt.y, d, d);
    }

    raf = requestAnimationFrame(frame);
  }

  function stat() {
    var d = dot();
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = fg;
    for (var k = 0; k < P.length; k++) {
      ctx.fillRect(P[k].hx, P[k].hy, d, d);
    }
  }

  function setP(e) {
    var r = canvas.getBoundingClientRect();
    pointer = {
      x: (e.clientX - r.left) * dpr,
      y: (e.clientY - r.top) * dpr
    };
  }

  function clP() {
    pointer = null;
  }

  stage.addEventListener('pointermove', setP);
  stage.addEventListener('pointerdown', setP);
  stage.addEventListener('pointerleave', clP);
  stage.addEventListener('pointercancel', clP);
  stage.addEventListener('pointerup', clP);

  var ro = new ResizeObserver(function() {
    build();
    if (reduce) stat();
  });

  ro.observe(stage);

  function clean() {
    ro.disconnect();
    stage.removeEventListener('pointermove', setP);
    stage.removeEventListener('pointerdown', setP);
    stage.removeEventListener('pointerleave', clP);
    stage.removeEventListener('pointercancel', clP);
    stage.removeEventListener('pointerup', clP);
  }

  build();
  if (reduce) stat();
  else raf = requestAnimationFrame(frame);
})();
</script>
```

## The real skill is prompting

If building is effectively free, your only leverage is the prompt. Nail it and you get exactly what you pictured on the first or second try. After a lot of reps, a handful of habits reliably turn a vague request into the exact thing in my head.

- **Name the tool and the destination.** I open with something like *"in the Sanity Studio using the MCP"* and say whether to push it as a draft. Claude should never have to guess where the work lands.
- **Pinpoint the exact spot.** Give the page slug, which module instance (say, *the 2nd hero.cover module*), which block, and even which field (the `css` field versus the `scopedCss` field). Precision here saves a dozen round trips.
- **Describe the visual like you mean it, and attach a reference.** *"An interactive pixelated dither background with some rainbow colors, like this"* plus an image gets me far closer than adjectives alone.
- **Spell out the interaction and the edge cases.** Should it react to a mouse hover and a finger drag on mobile? Should it settle down for reduced-motion users? Say so up front.
- **Iterate surgically.** Once it’s close, I make small scoped asks, like *"replace every --indicator with --crosshair in that block's css field"* instead of regenerating the whole thing.
- **Ask Claude to ask you questions first.** A simple *"ask me the right questions before you implement"* turns a one-shot guess into an actual collaboration.

None of those tips are about code. They are about being specific. The model handles the syntax. You supply the intent and you get something that would have taken a developer hours.

```sh
# Claude Code
Use the Sanity MCP. In the Sanity Studio, there is a page (slug=foo/bar) with a custom-html module. Keeping in draft state, in a deferred script tag and in an IIFE, generate a...

Ask me the right questions to implement successfully.
```

## You can build an entire page with it

Stack enough of these and you don't need any other module. A full page of `custom-html` blocks is a completely custom page: no preset layouts, no module constraints, every section described in plain language and vibe-coded into existence. Describe each one to Claude, let it generate the markup and styles, push to draft. The whole thing lives in Sanity and edits like any other page. Most people building sites today don't realize this is already available to them.

Stop sleeping on it, and start building.

The Custom HTML module closes the gap between *"I wish this page could do that"* and actually shipping it—permanently. Pair it with Claude and the Sanity MCP and the bottleneck stops being your skills and starts being your imagination. Every day you're not using it is a day you're leaving capability on the table. Go open a Custom HTML block right now and describe something you've always wanted to see on one of your sites.

*I wish this page could do that* and actually shipping it. Pair it with Claude and the Sanity MCP, and the bottleneck stops being your code and starts being your imagination. Go find a page, open a Custom HTML block, and describe something you have always wanted to see.

![first person fish-eye lens view from the perspective of the orange jellyfish, where my orange tentacles are reaching towards the sleeping man on the couch](https://cdn.sanity.io/images/cyu7k2r0/production/fb64d6568a47381701e7803c7ee5b269028c111f-1537x1023.png)

```css
[data-module="code"]:nth-of-type(2) .line {
  white-space: normal;
}
```
