arcgis-popup-templates
ArcGIS Popup Templates
Use this skill for creating and customizing popup templates with various content types.
PopupTemplate Overview
| 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 |
Basic PopupTemplate
layer.popupTemplate = {
title: "{name}",
content: "Population: {population}<br>Area: {area} sq mi"
};
With Field Substitution
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>
`
};
Content Array (Multiple Content Types)
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"]
}
}]
}
]
};
Content Types
TextContent
Display HTML or text.
{
type: "text",
text: `
<div style="padding: 10px;">
<h2>{name}</h2>
<p>{description}</p>
<a href="{website}" target="_blank">Visit Website</a>
</div>
`
}
FieldsContent
Display attributes as a table.
{
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"
}
},
{
fieldName: "percentage",
label: "Percentage",
format: {
places: 2,
digitSeparator: true
}
}
]
}
Date Formats
short-date- 12/30/2024short-date-short-time- 12/30/2024, 3:30 PMshort-date-short-time-24- 12/30/2024, 15:30short-date-long-time- 12/30/2024, 3:30:45 PMshort-date-long-time-24- 12/30/2024, 15:30:45long-month-day-year- December 30, 2024long-month-day-year-short-time- December 30, 2024, 3:30 PMlong-month-day-year-long-time- December 30, 2024, 3:30:45 PMday-short-month-year- 30 Dec 2024year- 2024
MediaContent
Display charts or images.
{
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
}
}
]
}
Chart Types
Bar Chart
{
type: "bar-chart",
title: "Population by Age",
value: {
fields: ["age_0_17", "age_18_34", "age_35_54", "age_55_plus"]
}
}
Pie Chart
{
type: "pie-chart",
title: "Land Use Distribution",
value: {
fields: ["residential", "commercial", "industrial", "agricultural"]
}
}
Line Chart
{
type: "line-chart",
title: "Monthly Sales",
value: {
fields: ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]
}
}
Column Chart
{
type: "column-chart",
title: "Revenue vs Expenses",
value: {
fields: ["revenue", "expenses"]
}
}
Image
{
type: "image",
title: "Property Photo",
value: {
sourceURL: "{image_url}",
linkURL: "{detail_page_url}"
}
}
// Fixed image
{
type: "image",
value: {
sourceURL: "https://example.com/logo.png"
}
}
AttachmentsContent
Display file attachments.
{
type: "attachments",
displayType: "preview", // preview, list, auto
title: "Photos"
}
// With custom display
{
type: "attachments",
displayType: "list",
title: "Documents",
description: "Related files for this record"
}
ExpressionContent
Display Arcade expression results.
// First, define expression in expressionInfos
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"
}
}
]
};
CustomContent
Create fully custom content with JavaScript.
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>
`;
// Add custom logic, charts, etc.
return div;
}
});
layer.popupTemplate = {
title: "{name}",
content: [customContent]
};
RelationshipContent
Display related records.
{
type: "relationship",
relationshipId: 0, // Relationship ID from the layer
title: "Related Inspections",
displayCount: 5,
orderByFields: [
{
field: "inspection_date",
order: "desc"
}
]
}
FieldInfo Configuration
Basic FieldInfo
const fieldInfo = {
fieldName: "population",
label: "Population",
visible: true,
isEditable: false,
statisticType: "sum" // For clustering: sum, min, max, avg, count
};
Number Formatting
{
fieldName: "revenue",
label: "Annual Revenue",
format: {
digitSeparator: true,
places: 2
}
}
// Currency (manual)
{
fieldName: "price",
label: "Price",
format: {
digitSeparator: true,
places: 2
}
}
// Use expression for currency symbol
Date Formatting
{
fieldName: "created_date",
label: "Created",
format: {
dateFormat: "long-month-day-year"
}
}
Tooltip
{
fieldName: "status",
label: "Status",
tooltip: "Current processing status of the request"
}
RelatedRecordsInfo
Configure how related records are displayed.
layer.popupTemplate = {
title: "{name}",
content: [{
type: "relationship",
relationshipId: 0
}],
relatedRecordsInfo: {
showRelatedRecords: true,
orderByFields: [{
field: "date",
order: "desc"
}]
}
};
Actions
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"
},
{
id: "delete",
title: "Delete",
className: "esri-icon-trash"
}
]
};
// Handle action clicks
view.popup.on("trigger-action", (event) => {
if (event.action.id === "zoom-to") {
view.goTo(view.popup.selectedFeature);
} else if (event.action.id === "edit") {
// Open editor
startEditing(view.popup.selectedFeature);
} else if (event.action.id === "delete") {
// Delete feature
deleteFeature(view.popup.selectedFeature);
}
});
Action Button Types
// Icon button
{
id: "info",
title: "More Info",
className: "esri-icon-description"
}
// Text button
{
id: "report",
title: "Generate Report",
type: "button"
}
// Toggle button
{
id: "highlight",
title: "Highlight",
type: "toggle",
value: false
}
Dynamic Content with Functions
Content as Function
layer.popupTemplate = {
title: "{name}",
content: (feature) => {
const attributes = feature.graphic.attributes;
// Conditional content
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>
<p>Zoning: ${attributes.zoning}</p>
`;
}
}
};
Async Content Function
layer.popupTemplate = {
title: "{name}",
content: async (feature) => {
const id = feature.graphic.attributes.OBJECTID;
// Fetch additional data
const response = await fetch(`/api/details/${id}`);
const data = await response.json();
return `
<h3>${data.title}</h3>
<p>${data.description}</p>
<img src="${data.imageUrl}" />
`;
}
};
Arcade Expressions
In Title
layer.popupTemplate = {
title: {
expression: `
var name = $feature.name;
var status = $feature.status;
return name + " (" + status + ")";
`
},
content: "..."
};
Expression Infos
layer.popupTemplate = {
expressionInfos: [
{
name: "formatted-date",
title: "Formatted Date",
expression: `
var d = $feature.created_date;
return Text(d, "MMMM D, YYYY");
`
},
{
name: "calculated-field",
title: "Density",
expression: "Round($feature.population / AreaGeodetic($feature, 'square-miles'), 1)"
},
{
name: "conditional-value",
title: "Status",
expression: `
var val = $feature.score;
if (val >= 80) return "Excellent";
if (val >= 60) return "Good";
if (val >= 40) return "Fair";
return "Poor";
`
}
],
content: [
{
type: "fields",
fieldInfos: [
{ fieldName: "expression/formatted-date", label: "Created" },
{ fieldName: "expression/calculated-field", label: "Population Density" },
{ fieldName: "expression/conditional-value", label: "Rating" }
]
}
]
};
OutFields
Specify which fields to retrieve.
layer.popupTemplate = {
title: "{name}",
content: "...",
outFields: ["name", "population", "area", "created_date"]
};
// All fields
layer.popupTemplate = {
title: "{name}",
content: "...",
outFields: ["*"]
};
Last Edit Info
Show who last edited the feature.
layer.popupTemplate = {
title: "{name}",
content: "...",
lastEditInfoEnabled: true
};
Popup Template from JSON
const popupTemplate = {
title: "{name}",
content: [{
type: "fields",
fieldInfos: [
{ fieldName: "category", label: "Category" },
{ fieldName: "value", label: "Value" }
]
}],
outFields: ["name", "category", "value"]
};
// Apply to layer
layer.popupTemplate = popupTemplate;
// Or create from class
import PopupTemplate from "@arcgis/core/PopupTemplate.js";
layer.popupTemplate = new PopupTemplate(popupTemplate);
Layer-Specific Popup
FeatureLayer with Popup
const featureLayer = new FeatureLayer({
url: "https://services.arcgis.com/.../FeatureServer/0",
popupTemplate: {
title: "{NAME}",
content: [{
type: "fields",
fieldInfos: [
{ fieldName: "NAME", label: "Name" },
{ fieldName: "POP2020", label: "Population (2020)", format: { digitSeparator: true } }
]
}]
}
});
GeoJSONLayer with Popup
const geoJsonLayer = new GeoJSONLayer({
url: "data.geojson",
popupTemplate: {
title: "{properties/name}", // Note: GeoJSON uses properties/fieldName
content: "{properties/description}"
}
});
GraphicsLayer Popup
const graphic = new Graphic({
geometry: point,
attributes: {
name: "Location A",
value: 100
},
popupTemplate: {
title: "{name}",
content: "Value: {value}"
}
});
graphicsLayer.add(graphic);
Clustering Popups
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 }
}
]
}]
},
clusterMinSize: 16,
clusterMaxSize: 60,
fields: [{
name: "cluster_avg_population",
alias: "Average Population",
onStatisticField: "population",
statisticType: "avg"
}]
};
Common Patterns
Popup with Image Gallery
layer.popupTemplate = {
title: "{name}",
content: [
{
type: "text",
text: "{description}"
},
{
type: "attachments",
displayType: "preview"
}
]
};
Multi-Tab Style Popup
layer.popupTemplate = {
title: "{name}",
content: [
{
type: "text",
text: "<b>General Information</b>"
},
{
type: "fields",
fieldInfos: [
{ fieldName: "category", label: "Category" },
{ fieldName: "status", label: "Status" }
]
},
{
type: "text",
text: "<hr><b>Statistics</b>"
},
{
type: "media",
mediaInfos: [{
type: "pie-chart",
title: "Distribution",
value: { fields: ["typeA", "typeB", "typeC"] }
}]
}
]
};
Conditional Content
layer.popupTemplate = {
title: "{name}",
expressionInfos: [{
name: "show-warning",
expression: "$feature.risk_level > 7"
}],
content: (feature) => {
const content = [{
type: "fields",
fieldInfos: [
{ fieldName: "name", label: "Name" },
{ fieldName: "risk_level", label: "Risk Level" }
]
}];
if (feature.graphic.attributes.risk_level > 7) {
content.unshift({
type: "text",
text: '<div style="background: #ffcccc; padding: 10px;">⚠️ High Risk Area</div>'
});
}
return content;
}
};
TypeScript Usage
PopupTemplate content uses autocasting with type properties. For TypeScript safety, use as const:
// Use 'as const' for type safety
layer.popupTemplate = {
title: "{name}",
content: [
{
type: "fields",
fieldInfos: [
{ fieldName: "name", label: "Name" },
{ fieldName: "population", label: "Population" }
]
},
{
type: "media",
mediaInfos: [{
type: "pie-chart",
value: { fields: ["typeA", "typeB"] }
}]
}
]
} as const;
Tip: See arcgis-core-maps skill for detailed guidance on autocasting vs explicit classes.
Reference Samples
intro-popuptemplate- Basic PopupTemplate configurationpopup-actions- Adding custom actions to popupspopup-customcontent- Custom popup content elementspopuptemplate-arcade- Using Arcade expressions in popupspopup-multipleelements- Multiple content elements in popups
Common Pitfalls
-
Field 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
popupTemplate: { title: "{name}", content: "{description}", outFields: ["name", "description"] // Both required } -
GeoJSON Field Path: GeoJSON requires
properties/prefix// GeoJSON title: "{properties/name}" // Regular FeatureLayer title: "{name}" -
Expression Reference: Use
expression/prefix for Arcade expressionsfieldInfos: [ { fieldName: "expression/my-expression", label: "Calculated" } ] -
Async Content: 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; }