Join us at FabCon Atlanta from March 16 - 20, 2026, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.
Register now!The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now! Learn more
Introduction
When working with multiple Power BI reports, it’s often useful to synchronize slicers across them .
This guide walks you through how to build an HTML+JavaScript web page that does exactly that: two Power BI reports displayed side-by-side, with slicers in Report 1 automatically applying corresponding filters to Report 2 in real-time.
Let’s break it down step by step.
🧠 Prerequisites
Before you start, make sure you have:
💻 Full Code
We’ll start with the full HTML file (later we’ll explain each section in detail).
👉 Save this as index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Power BI - Dual Slicer Cross Report Filtering</title>
<script src="https://alcdn.msauth.net/browser/2.35.0/js/msal-browser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/powerbi-client@2.22.3/dist/powerbi.js"></script>
<style>
body { font-family: Arial, sans-serif; }
#reportsContainer { display: flex; gap: 20px; margin-top: 20px; }
.reportBox { flex: 1; height: 540px; border:1px solid #ccc; }
button { margin-right: 10px; margin-bottom: 10px; }
</style>
</head>
<body>
<h2>Power BI Reports Cross Filtering (Two Slicers)</h2>
<button id="signInButton">Sign In & Load Reports</button>
<button id="listSlicersButton" disabled>List Slicers in Report 1</button>
<button id="listVisualsButton" disabled>List Visual IDs in Report 2</button>
<div id="reportsContainer">
<div id="reportContainerLeft" class="reportBox"></div>
<div id="reportContainerRight" class="reportBox"></div>
</div>
<script>
const clientId = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const tenantId = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const reportId1 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const groupId1 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const reportId2 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const groupId2 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const msalInstance = new msal.PublicClientApplication({
auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, redirectUri: window.location.origin }
});
const scopes = ["https://analysis.windows.net/powerbi/api/.default"];
let report1, report2;
const models = window['powerbi-client'].models;
// === Sign In & Embed Both Reports ===
document.getElementById("signInButton").onclick = async () => {
try {
const loginResponse = await msalInstance.loginPopup({ scopes });
const tokenResponse = await msalInstance.acquireTokenSilent({ scopes, account: loginResponse.account });
const accessToken = tokenResponse.accessToken;
report1 = powerbi.embed(document.getElementById("reportContainerLeft"), {
type: "report",
tokenType: models.TokenType.Aad,
accessToken,
id: reportId1,
embedUrl:`https://app.powerbi.com/reportEmbed?reportId=${reportId1}&groupId=${groupId1}`,
settings: { panes: { filters: { visible: false } } }
});
report2 = powerbi.embed(document.getElementById("reportContainerRight"), {
type: "report",
tokenType: models.TokenType.Aad,
accessToken,
id: reportId2,
embedUrl:`https://app.powerbi.com/reportEmbed?reportId=${reportId2}&groupId=${groupId2}`,
settings: { panes: { filters: { visible: false } } }
});
report1.on("loaded", () => {
console.log("✅ Report 1 loaded");
document.getElementById("listSlicersButton").disabled = false;
document.getElementById("listVisualsButton").disabled = false;
setupDualSlicerListener();
});
report2.on("loaded", () => console.log("✅ Report 2 loaded"));
} catch(err){
console.error("❌ Authentication failed:", err);
}
};
// === List slicers in Report 1 ===
document.getElementById("listSlicersButton").onclick = async () => {
if(!report1) return;
const pages = await report1.getPages();
for(let page of pages){
await page.setActive();
const visuals = await page.getVisuals();
for(let visual of visuals){
if(visual.type === "slicer"){
console.log(`Slicer title: "${visual.title}", ID: "${visual.name}"`);
}
}
}
};
// === List visuals in Report 2 ===
document.getElementById("listVisualsButton").onclick = async () => {
if(!report2) return;
const pages = await report2.getPages();
const page = pages.find(p => p.isActive) || pages[0];
await page.setActive();
const visuals = await page.getVisuals();
for(let visual of visuals){
console.log(`Visual title: "${visual.title}", name: "${visual.name}", type: "${visual.type}"`);
}
};
// === Dual slicer polling listener ===
async function setupDualSlicerListener() {
const pages1 = await report1.getPages();
const page1 = pages1.find(p => p.isActive) || pages1[0];
const visuals1 = await page1.getVisuals();
// Slicer 1 → Country
const slicerCountry = visuals1.find(v => v.type === "slicer" && v.name === "aebf94a0ac0055d793ea");
// Slicer 2 → Age
const slicerAge = visuals1.find(v => v.type === "slicer" && v.name === "881d066a54a44db7a330");
if(!slicerCountry || !slicerAge) {
console.error("❌ One or both slicers not found!");
return;
}
let lastCountryValues = [];
let lastAgeValues = [];
setInterval(async () => {
try {
const [countryState, ageState] = await Promise.all([
slicerCountry.getSlicerState(),
slicerAge.getSlicerState()
]);
const countryValues = countryState.filters.map(f => f.values).flat();
const ageValues = ageState.filters.map(f => f.values).flat();
if (JSON.stringify(countryValues) !== JSON.stringify(lastCountryValues) ||
JSON.stringify(ageValues) !== JSON.stringify(lastAgeValues)) {
lastCountryValues = countryValues;
lastAgeValues = ageValues;
console.log("🎯 Slicer change detected:", { countryValues, ageValues });
const pages2 = await report2.getPages();
const page2 = pages2.find(p => p.isActive) || pages2[0];
const visuals2 = await page2.getVisuals();
const tableVisual = visuals2.find(v => v.name === "8567ad09be521ec1295a");
if(!tableVisual) { console.error("❌ Table visual not found in report 2"); return; }
// Remove all filters first
await tableVisual.updateFilters(models.FiltersOperations.RemoveAll);
// Build and apply combined filters
const filters = [];
if(countryValues.length > 0) {
filters.push({
$schema: "http://powerbi.com/product/schema#basic",
filterType: 1,
target: { table: "Table", column: "Country" },
operator: "In",
values: countryValues,
requireSingleSelection: false
});
}
if(ageValues.length > 0) {
filters.push({
$schema: "http://powerbi.com/product/schema#basic",
filterType: 1,
target: { table: "Table", column: "Age" },
operator: "In",
values: ageValues,
requireSingleSelection: false
});
}
if(filters.length > 0) {
await tableVisual.updateFilters(models.FiltersOperations.Add, filters);
console.log("✅ Applied filters to Report 2:", filters);
} else {
console.log("✅ Cleared filters from Report 2 (no slicer selections)");
}
}
} catch(err) { console.error(err); }
}, 1000);
}
</script>
</body>
</html>
🔍 Code Breakdown
Let’s walk through what each section does:
🏗️ 1. HTML & Style Setup
<div id="reportsContainer">
<div id="reportContainerLeft" class="reportBox"></div>
<div id="reportContainerRight" class="reportBox"></div>
</div>
This sets up two boxes side by side — one for each Power BI report.
The CSS display: flex ensures they appear next to each other.
🔐 2. Configuration & Authentication (MSAL)
const clientId = "...";
const tenantId = "...";
const msalInstance = new msal.PublicClientApplication({ ... });
When you click “Sign In & Load Reports”, the app opens a Microsoft sign-in popup and requests a Power BI access token (https://analysis.windows.net/powerbi/api/.default).
📊 3. Embedding Power BI Reports
report1 = powerbi.embed(document.getElementById("reportContainerLeft"), { ... });
report2 = powerbi.embed(document.getElementById("reportContainerRight"), { ... });
Once the user is authenticated, both reports are embedded using the Power BI JavaScript SDK.
Each report is loaded via its embed URL, which includes the reportId and groupId.
🧾 4. Listing Slicers & Visuals
const visuals = await page.getVisuals();
if(visual.type === "slicer") console.log(visual.name);
These two buttons are diagnostic helpers:
You’ll need these IDs to target specific visuals in the filtering logic.
⚙️ 5. Dual Slicer Listener
The most powerful part of the script.
setInterval(async () => {
const countryState = await slicerCountry.getSlicerState();
const ageState = await slicerAge.getSlicerState();
...
}, 1000);
This runs every 1 second:
🧩 6. Applying Filters to the Second Report
await tableVisual.updateFilters(models.FiltersOperations.RemoveAll);
await tableVisual.updateFilters(models.FiltersOperations.Add, filters);
The code:
The filter schema used is the Power BI Basic filter format:
{
$schema: "http://powerbi.com/product/schema#basic",
filterType: 1,
target: { table: "Table", column: "Country" },
operator: "In",
values: ["USA", "Canada"]
}
🚀 Running the Code with a Python Server
Power BI embedding requires your app to run on a local or hosted server (not a file:// path).
You can use Python’s built-in web server to serve this HTML file.
Step 1 — Save as index.html
Save the code into a folder, e.g.:
C:\powerbi-dual-slicer\
Step 2 — Run Server
In that folder, run:
python -m http.server 5500
Step 3 — Open in Browser
Go to:
http://localhost:5500/index.html
Click Sign In & Load Reports, and once you authenticate, both reports will load.
🧰 Troubleshooting
|
Issue |
Possible Fix |
|
Authentication failed |
Ensure redirect URI matches http://localhost:8000 in Azure App registration |
|
Slicer not found |
Use “List Slicers” button to check slicer IDs |
|
Table not filtered |
Verify visual ID (tableVisual.name) in Report 2 |
|
Blank screen |
Must serve the file using a web server (not file://) |
🏁 Conclusion
This project demonstrates a powerful pattern — cross-report communication using only JavaScript, MSAL authentication, and Power BI’s public client API.
You can extend this by:
I have attached the two reports i have used for this demo for your reference.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.