
arcgis-popup-templates
by SaschaBrunnerCH
Install AI assistant context files for ArcGIS Maps SDK for JavaScript development. Supports Claude skills and GitHub Copilot instructions.
SKILL.md
name: arcgis-popup-templates description: Configure rich popup content with text, fields, media, charts, attachments, and related records. Use for customizing feature popups with dynamic content, expressions, and interactive elements.
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.
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; }
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です