Stand with Ukraine 🇺🇦
Products
Services
Blog
About Us

HTML Macro

User Macro in Confluence Cloud provides a flexible way to render custom UI directly inside Confluence pages. A key capability is that macros can output HTML, CSS, and JavaScript, depending on macro type and sandbox constraints.

This makes it possible to build lightweight widgets, dashboards, embeds, and interactive components directly inside Confluence pages.


Rendering HTML from Macro Template or Body

You can output HTML either:

{"timestamp":1792799573428,"msg":"A unknown Exception Occurred","errorMsg":"The provided AtlassianHostUser did not specify a user to act as.","code":"500"}

Using HTML, CSS, and JavaScript Together

User Macro for Confluence Cloud supports:

✅ HTML

Full markup is supported inside templates or macro body.

✅ CSS

You can use:

  • <style> tags

  • inline styles

  • external CDN stylesheets

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">

<style>
  .box {
    padding: 12px;
    border: 1px solid #ddd;
  }
</style>

✅ JavaScript

JavaScript can be used inside <script> tags, but behavior depends on macro type.


Static vs Dynamic Content Macros

Static Macros

Static macros are strictly limited:

  • ❌ JavaScript is NOT allowed

  • ❌ Scripts are stripped during rendering

  • ❌ Only safe HTML output is permitted

This is enforced by Confluence Storage Format rules.

👉 Use Static macros when:

  • you only need layout rendering

  • no interactivity is required


Dynamic Macros

Dynamic macros allow interactivity:

  • ✅ JavaScript is allowed

  • ✅ Runs inside a sandboxed iframe

Scripts execute in a controlled environment to ensure instance security.

👉 Use Dynamic macros when:

  • you need UI interactivity

  • you want to fetch data or update UI dynamically


Combining Server-Side Velocity + Client-Side JS

Velocity handles server-side rendering, while JavaScript runs in the browser.

You can pass Velocity variables into JS directly:

#set($message = "Hello from Velocity")

<script>
  const message = "$message";
  console.log(message);
</script>

A practical example:

Example showing server-side API call + client-side fetching:

## Specify a label name parameter
#set($label = $parameters["label"])

#if(!$label)
<div class="aui-message aui-message-info">
    Please provide a <code>label</code> parameter.
</div>
#else

## Fetch pages for a specific label
#set($response = $ConfluenceManager.get("/wiki/rest/api/content/search?cql=type=page AND label='$label'"))
#set($results = $response.results)
#set($base = $response._links.base)

<div id="label-header" data-label="$!label" style="display:flex; align-items:center; gap:12px; margin-bottom:8px;">
    <div>
        <span class="aui-lozenge aui-lozenge-subtle aui-lozenge-inprogress">Showing pages for label:</span> <span class="aui-lozenge aui-lozenge-subtle aui-lozenge-new">$!label</span>
    </div>

    <button title="Download CSV" id="download-csv-btn"
        type="button"
        aria-label="Download CSV"
        style="
        display:inline-flex;
        align-items:center;
        gap:8px;
        border:0;
        border-radius:4px;
        padding:6px 12px;
        cursor:pointer;
        font-family:inherit;
        font-size:14px;
        ">
        <span class="iconify" data-icon="ph:download" data-inline="false" style="font-size:18px;"></span>
    </button>
</div>

## Create a list of pages
#if($response)
<ul id="label-pages-list" style="padding-left:18px;">
    #foreach($page in $results)
    #set($title = $page.title)
    #set($webui = $page._links.webui)
    #set($href = "${base}$webui")
    <li data-title="$!title" style="list-style-type: circle;"><a href="$!href" target="_blank" rel="noopener noreferrer">$!title</a></li>
    #end
</ul>
#else
<div>
    No pages found for label <code>$!label</code>.
</div>
#end

<script type="module">
    (function () {
        const btn = document.getElementById('download-csv-btn');
        const list = document.getElementById('label-pages-list');
        const header = document.getElementById('label-header');
        if (!btn) return;

        const label = (header && header.dataset && header.dataset.label) ? header.dataset.label: 'pages';
        const safeFilename = (name) => String(name).replace(/\s+/g, '_') + '-pages.csv';


        const escapeCsvField = (s) => {
            if (s == null) return '';
            s = String(s);

            if (/[",\r\n]/.test(s)) {
                return '"' + s.replace(/"/g, '""') + '"';
            }

            return s;
        };

        const buildCsv = () => {
            const rows = [];
            rows.push(['Title', 'URL'].map(escapeCsvField).join(','));
            if (!list) return rows.join('\r\n');

            for (const li of list.querySelectorAll('li')) {
                const a = li.querySelector('a');
                const title = li.getAttribute('data-title') || (a ? a.textContent.trim(): '');
                const url = a ? a.href: '';
                rows.push([title, url].map(escapeCsvField).join(','));
            }
            return rows.join('\r\n');
        };

        const downloadBlob = (content, filename) => {
            const blob = new Blob([content], {
                type: 'text/csv;charset=utf-8;'
            });
            if (navigator.msSaveBlob) {
                navigator.msSaveBlob(blob, filename);
                return;
            }
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.setAttribute('download', filename);
            a.style.display = 'none';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        };

        btn.addEventListener('click', (e) => {
            e.preventDefault();
            const csv = buildCsv();
            const filename = safeFilename(label);
            downloadBlob(csv, filename);
        });
    })();
</script>

<script>
    if (!window.iconifyOnLoad) {
        (function() {
            const s = document.createElement('script');
            s.src = 'https://code.iconify.design/2/2.2.1/iconify.min.js';
            s.defer = true;
            document.head.appendChild(s);
            window.iconifyOnLoad = true;
        })();
    }
</script>

#end

iFrame Sandboxing in Dynamic Macros

Dynamic macros run inside nested iframes:

  • outer iframe: Confluence macro container

  • inner iframe: sandboxed execution environment

Restrictions include:

  • limited DOM access outside macro

  • controlled API usage via Atlassian APIs

  • secure isolation of scripts

To compensate, use:


Using Atlassian UI (AUI) + AJS

AUI is available by default via CDN, enabling consistent styling.

Example: Button with loading state

<button class="aui-button aui-button-primary" id="loadingButton">
  Make request
</button>

<script>
  AJS.$(document).on("click", "#loadingButton", function () {
    var that = this;

    if (!that.isBusy()) {
      that.busy();

      AP.request("/rest/api/user/current")
        .then((response) => {
          console.log({ response });
          that.idle();
        });
    }
  });
</script>

Read more about styling Confluence macros here


Embedding External Tools (Google Calendar, etc.)

You can embed external services using <iframe>.

Example: Google Calendar embed

<iframe
  src="https://calendar.google.com/calendar/embed?src=your_calendar_id"
  style="border: 0"
  width="800"
  height="600"
  frameborder="0"
  scrolling="no">
</iframe>
Screenshot 2026-06-22 at 14.06.52.png

This allows embedding:

  • Google Calendar

  • dashboards

  • internal tools

  • reporting systems

  • third-party widgets