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:
directly from the macro template
or from the macro body using
$bodywith plain-text body type
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>tagsinline 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:
AJS(Atlassian UI helpers)
Read more about dynamic macros here
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>
This allows embedding:
Google Calendar
dashboards
internal tools
reporting systems
third-party widgets