skills/arcgis-popup-templates/SKILL.md
Configure rich popup content with text, fields, media, charts, attachments, and related records. Use when customizing feature popups, adding charts or images to popups, templating popup titles and field formatting, or displaying related record data on click.
npx skillsauth add SaschaBrunnerCH/arcgis-maps-sdk-js-ai-context arcgis-popup-templatesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill for creating and customizing popup templates with various content types.
import PopupTemplate from "@arcgis/core/PopupTemplate.js";
import CustomContent from "@arcgis/core/popup/content/CustomContent.js";
const PopupTemplate = await $arcgis.import("@arcgis/core/PopupTemplate.js");
const CustomContent = await $arcgis.import(
"@arcgis/core/popup/content/CustomContent.js",
);
Note: The examples in this skill use Direct ESM imports. For CDN usage, replace
import X from "path"withconst X = await $arcgis.import("path").
| Content Type | Purpose | | ------------------- | ------------------------- | | TextContent | HTML or plain text | | FieldsContent | Attribute table | | MediaContent | Charts and images | | AttachmentsContent | File attachments | | ExpressionContent | Arcade expression results | | CustomContent | Custom HTML/JavaScript | | RelationshipContent | Related records |
| Property | Type | Description |
| ------------------ | -------------------------------------- | --------------------------------------------- |
| title | string | Function | object | Title with field substitution ({fieldName}) |
| content | string | Array | Function | Promise | Content definition |
| fieldInfos | FieldInfo[] | Default field formatting |
| expressionInfos | ExpressionInfo[] | Arcade expression definitions |
| outFields | string[] | Fields to retrieve for popup |
| actions | ActionButton[] | ActionToggle[] | Custom action buttons |
| overwriteActions | boolean | Replace default popup actions |
| returnGeometry | boolean | Include geometry in popup results |
layer.popupTemplate = {
title: "{name}",
content: "Population: {population}<br>Area: {area} sq mi",
};
layer.popupTemplate = {
title: "{city_name}, {state}",
content: `
<h3>Demographics</h3>
<p>Population: {population:NumberFormat(places: 0)}</p>
<p>Median Income: {median_income:NumberFormat(digitSeparator: true, places: 0)}</p>
<p>Founded: {founded_date:DateFormat(selector: 'date', datePattern: 'MMMM d, yyyy')}</p>
`,
};
layer.popupTemplate = {
title: "{name}",
content: [
{
type: "text",
text: "<b>Overview</b><br>{description}",
},
{
type: "fields",
fieldInfos: [
{ fieldName: "population", label: "Population" },
{ fieldName: "area", label: "Area (sq mi)" },
],
},
{
type: "media",
mediaInfos: [
{
type: "pie-chart",
title: "Demographics",
value: {
fields: ["white", "black", "asian", "other"],
},
},
],
},
],
};
{
type: "text",
text: `
<div style="padding: 10px;">
<h2>{name}</h2>
<p>{description}</p>
<a href="{website}" target="_blank">Visit Website</a>
</div>
`
}
{
type: "fields",
fieldInfos: [
{
fieldName: "name",
label: "Name"
},
{
fieldName: "population",
label: "Population",
format: {
digitSeparator: true,
places: 0
}
},
{
fieldName: "date_created",
label: "Created",
format: {
dateFormat: "short-date"
}
}
]
}
short-date - 12/30/2024short-date-short-time - 12/30/2024, 3:30 PMshort-date-long-time - 12/30/2024, 3:30:45 PMlong-month-day-year - December 30, 2024day-short-month-year - 30 Dec 2024year - 2024{
type: "media",
mediaInfos: [
{
title: "Sales by Quarter",
type: "column-chart", // bar-chart, pie-chart, line-chart, column-chart, image
value: {
fields: ["q1_sales", "q2_sales", "q3_sales", "q4_sales"],
normalizeField: "total_sales" // Optional
}
}
]
}
| Type | Use Case |
| -------------- | ------------------------------------------ |
| bar-chart | Horizontal bars for categorical comparison |
| pie-chart | Proportional distribution |
| line-chart | Trends over series |
| column-chart | Vertical bars for comparison |
| image | Display images from URL fields |
Image MediaInfo:
{
type: "image",
title: "Property Photo",
value: {
sourceURL: "{image_url}",
linkURL: "{detail_page_url}"
}
}
{
type: "attachments",
displayType: "preview", // preview, list, auto
title: "Photos"
}
layer.popupTemplate = {
expressionInfos: [
{
name: "population-density",
title: "Population Density",
expression: "Round($feature.population / $feature.area, 2)",
},
{
name: "age-category",
title: "Age Category",
expression: `
var age = $feature.building_age;
if (age < 25) return "New";
if (age < 50) return "Moderate";
return "Historic";
`,
},
],
content: [
{
type: "expression",
expressionInfo: {
name: "population-density",
},
},
],
};
import CustomContent from "@arcgis/core/popup/content/CustomContent.js";
const customContent = new CustomContent({
outFields: ["*"],
creator: (event) => {
const div = document.createElement("div");
const graphic = event.graphic;
div.innerHTML = `
<div class="custom-popup">
<h3>${graphic.attributes.name}</h3>
<canvas id="chart-${graphic.attributes.OBJECTID}"></canvas>
</div>
`;
return div;
},
});
layer.popupTemplate = {
title: "{name}",
content: [customContent],
};
{
type: "relationship",
relationshipId: 0,
title: "Related Inspections",
displayCount: 5,
orderByFields: [
{
field: "inspection_date",
order: "desc"
}
]
}
The <arcgis-popup> component provides popup display control.
Key Properties:
| Property | Type | Description |
| ---------------------------------- | ------------------------ | ------------------------------ |
| actions | Collection | Custom action buttons |
| content | string | Node | Widget | Popup content |
| dock-options | object | Docking behavior configuration |
| features | Graphic[] | Features to display |
| heading | string | Popup heading text |
| heading-level | number | Heading level (1-6) |
| include-default-actions-disabled | boolean | Disable default zoom-to action |
| initial-display-mode | string | Initial display mode |
| location | Point | Popup anchor location |
| open | boolean | Whether popup is open |
| selected-feature | Graphic | Currently selected feature |
| selected-feature-index | number | Index of selected feature |
Key Events:
| Event | Description |
| --------------------- | ------------------------------------- |
| arcgisTriggerAction | Fires when a custom action is clicked |
Add custom buttons to popups.
layer.popupTemplate = {
title: "{name}",
content: "...",
actions: [
{
id: "zoom-to",
title: "Zoom To",
className: "esri-icon-zoom-in-magnifying-glass",
},
{
id: "edit",
title: "Edit",
className: "esri-icon-edit",
},
],
};
// Handle action clicks using reactiveUtils
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
reactiveUtils.on(
() => view.popup,
"trigger-action",
(event) => {
if (event.action.id === "zoom-to") {
view.goTo(view.popup.selectedFeature);
} else if (event.action.id === "edit") {
startEditing(view.popup.selectedFeature);
}
},
);
// Icon button
{ id: "info", title: "More Info", className: "esri-icon-description" }
// Toggle button
{ id: "highlight", title: "Highlight", type: "toggle", value: false }
layer.popupTemplate = {
title: "{name}",
outFields: ["*"],
content: (feature) => {
const attributes = feature.graphic.attributes;
if (attributes.type === "residential") {
return `
<h3>Residential Property</h3>
<p>Bedrooms: ${attributes.bedrooms}</p>
<p>Bathrooms: ${attributes.bathrooms}</p>
`;
} else {
return `
<h3>Commercial Property</h3>
<p>Square Footage: ${attributes.sqft}</p>
`;
}
},
};
layer.popupTemplate = {
title: "{name}",
outFields: ["*"],
content: async (feature) => {
const id = feature.graphic.attributes.OBJECTID;
const response = await fetch(`/api/details/${id}`);
const data = await response.json();
return `
<h3>${data.title}</h3>
<p>${data.description}</p>
`;
},
};
layer.popupTemplate = {
title: {
expression: `
var name = $feature.name;
var status = $feature.status;
return name + " (" + status + ")";
`,
},
content: "...",
};
layer.popupTemplate = {
expressionInfos: [
{
name: "formatted-date",
title: "Formatted Date",
expression: 'Text($feature.created_date, "MMMM D, YYYY")',
},
{
name: "calculated-field",
title: "Density",
expression:
"Round($feature.population / AreaGeodetic($feature, 'square-miles'), 1)",
},
],
content: [
{
type: "fields",
fieldInfos: [
{ fieldName: "expression/formatted-date", label: "Created" },
{
fieldName: "expression/calculated-field",
label: "Population Density",
},
],
},
],
};
layer.popupTemplate = {
title: "{name}",
content: "...",
outFields: ["name", "population", "area", "created_date"],
};
// All fields
layer.popupTemplate = {
title: "{name}",
content: "...",
outFields: ["*"],
};
layer.featureReduction = {
type: "cluster",
clusterRadius: 80,
popupTemplate: {
title: "Cluster of {cluster_count} features",
content: [
{
type: "fields",
fieldInfos: [
{
fieldName: "cluster_count",
label: "Features in cluster",
},
{
fieldName: "cluster_avg_population",
label: "Average Population",
format: { digitSeparator: true, places: 0 },
},
],
},
],
},
fields: [
{
name: "cluster_avg_population",
alias: "Average Population",
onStatisticField: "population",
statisticType: "avg",
},
],
};
<!DOCTYPE html>
<html>
<head>
<script src="https://js.arcgis.com/5.0/"></script>
<script
type="module"
src="https://js.arcgis.com/5.0/map-components/"
></script>
<style>
html,
body {
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<arcgis-map basemap="gray-vector" center="-73.95,40.70" zoom="11">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-legend slot="bottom-left"></arcgis-legend>
</arcgis-map>
<script type="module">
const FeatureLayer = await $arcgis.import(
"@arcgis/core/layers/FeatureLayer.js",
);
const mapElement = document.querySelector("arcgis-map");
const view = await mapElement.view;
await view.when();
const template = {
title: "Marriage in {NAME} Census Tract {TRACT}",
content: [
{
type: "fields",
fieldInfos: [
{
fieldName: "B12001_calc_pctMarriedE",
label: "Married %",
format: { digitSeparator: true, places: 1 },
},
{
fieldName: "B12001_calc_pctNeverE",
label: "Never Married %",
format: { digitSeparator: true, places: 1 },
},
],
},
],
};
const featureLayer = new FeatureLayer({
url: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/ACS_Marital_Status_Boundaries/FeatureServer/2",
popupTemplate: template,
});
mapElement.map.add(featureLayer);
</script>
</body>
</html>
intro-popuptemplate - Basic PopupTemplate configurationget-started-popuptemplate - Getting started with PopupTemplatepopup-actions - Adding custom actions to popupspopup-custom-action - Custom popup actions with geometry operatorspopup-customcontent - Custom popup content elementspopuptemplate-arcade - Using Arcade expressions in popupspopuptemplate-arcade-expression-content - Arcade expression contentpopup-multipleelements - Multiple content elements in popupspopuptemplate-function - Function-based popup contentpopuptemplate-promise - Promise-based popup contentpopuptemplate-browse-related-records - Related records in popupsField Names Case Sensitive: Field names must match exactly.
// If field is "Population" (capital P)
content: "{Population}"; // Correct
content: "{population}"; // Wrong - shows literal {population}
OutFields Required: Fields used in popup must be in outFields when using function content.
popupTemplate: {
title: "{name}",
outFields: ["name", "description"], // Required for function content
content: (feature) => {
return feature.graphic.attributes.description;
}
}
Expression Reference: Use expression/ prefix for Arcade expressions in fieldInfos.
fieldInfos: [{ fieldName: "expression/my-expression", label: "Calculated" }];
Async Content Must Return: Function content must return a value or Promise.
// Wrong - no return
content: (feature) => {
const div = document.createElement("div");
};
// Correct
content: (feature) => {
const div = document.createElement("div");
return div;
};
GeoJSON Field Path: GeoJSON requires properties/ prefix for field names.
// GeoJSON
title: "{properties/name}";
// Regular FeatureLayer
title: "{name}";
arcgis-interaction for hit testing and event handling.arcgis-editing for feature editing workflows.arcgis-arcade for detailed Arcade expression syntax.development
Build map user interfaces with ArcGIS widgets, Map Components, and Calcite Design System. Use for adding legends, layer lists, search, tables, time sliders, and custom UI layouts.
development
Add specialized widgets including BuildingExplorer, FloorFilter, Track, Locate, HistogramRangeSlider, ScaleBar, Compass, NavigationToggle, and media viewers. Use for building exploration, indoor mapping, GPS tracking, and data histograms.
data-ai
Style and render geographic data with renderers, symbols, and visual variables. Use for creating thematic maps, heatmaps, class breaks, unique values, labels, and 3D visualization.
tools
Work with ArcGIS Utility Networks for modeling and analyzing connected infrastructure including network tracing, associations visualization, and topology validation. Use for electric, gas, water, and telecom network analysis.