Display a Popup on clustered and unclustered Features in the same map

OpenLayers 3.7.0, cluster, popup, feature

Click on a Country, a Cluster point (labeled with number greater than 1) and a point labeled with the Number 1

In this example a Popup will show either a list of all "Sub-Features" (when it's within a cluster) or properties from the feature when it's not within a cluster. Also, it will display a popup, even if the feature is not clustered at the moment (in a visual way).

Ol3 offers a clustering-feature, which allows to keep the map clean on lower zoom levels when you got a layer with a lot of single features.

To get a single-features name displayed on popups you need a function which "digs" through the Cluster and all its containing features. Having a Popup-possibility for unclustered features (like in the default popup-example) COMBINED with a cluster-feature-popup needs an additionally logic.

When combining the default Popup-Example and the Cluster-Example from the repository it's necessary to know that the clustered features are part of the new created Cluster-Object.

Problem here: On lower zoom-levels the clustered features are displayed in a non-clustered way, like "normal" features. But still, they are part of the parent-cluster-object.

Solution: Add a simple typeof-construction to check if the current clicked feature is part of a cluster or not.


More custom examples

<!DOCTYPE html>
<title>Popups on clusterd and unclusterd Features</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/ol3/3.7.0/ol.css" type="text/css">
<script src="//cdnjs.cloudflare.com/ajax/libs/ol3/3.7.0/ol.js"></script>

	.popup {
	border: 1px solid #CCC;
	padding: 0.1em 1em 0.5em 0.5em;

<div class="container-fluid">
 <div class="row-fluid">
  <h3>Display a Popup on clustered and unclustered Features in the same map</h3>
  <div class="col-lg-12 col-md-12 col-sm-12">
  <div id="map" class="map"></div>
	<div id="popup" class="popup">
		<a href="/" id="popup-closer">Close</a>
		<div id="popup-content"></div>
function elem_id(id) {
	return document.getElementById(id);

var view = new ol.View({
    center: [0, 0],
    zoom: 2

var count = 100;
var features = new Array(count);
var e = 4500000;
for (var i = 0; i < count; ++i) {
    var coordinates = [2 * e * Math.random() - e, 2 * e * Math.random() - e];
    features[i] = new ol.Feature({
        geometry: new ol.geom.Point(coordinates),
        name: 'random' + [i]

var source = new ol.source.Vector({
    features: features

var clusterSource = new ol.source.Cluster({
    distance: 40,
    source: source

var styleCache = {};
var clusters = new ol.layer.Vector({
    source: clusterSource,
    style: function (feature, resolution) {
        var size = feature.get('features').length;
        var style = styleCache[size];
        if (!style) {
            style = [new ol.style.Style({
                image: new ol.style.Circle({
                    radius: 10,
                    stroke: new ol.style.Stroke({
                        color: '#fff'
                    fill: new ol.style.Fill({
                        color: '#3399CC'
                text: new ol.style.Text({
                    text: size.toString(),
                    fill: new ol.style.Fill({
                        color: '#fff'
            styleCache[size] = style;
        return style;

var map = new ol.Map({
    layers: [clusters,
    new ol.layer.Vector({
        source: new ol.source.Vector({
            url: 'data/countries.geojson',
            format: new ol.format.GeoJSON()

    target: 'map',
    controls: ol.control.defaults({
        attributionOptions: /** @type {olx.control.AttributionOptions} */
            collapsible: false
    view: view

var popup = elem_id('popup');
var popup_closer = elem_id('popup-closer');
var popup_content = elem_id('popup-content');
var olpopup = new ol.Overlay({
    element: popup,
    autoPan: false
popup_closer.onclick = function () {
    return false;
var OpenPopup = function (evt) {
    var feature = map.forEachFeatureAtPixel(evt.pixel,
    function (feature, layer) {
        if (feature) {
            var coord = map.getCoordinateFromPixel(evt.pixel);
            if (typeof feature.get('features') === 'undefined') {
                popup_content.innerHTML = '<h5><b>' + feature.get('name') + '</b></h5><i>this is an <b>unclustered</b> feature</i>';
            } else {
                var cfeatures = feature.get('features');
                if (cfeatures.length > 1) {
                    popup_content.innerHTML = '<h5><strong>all "Sub-Features"</strong></h5>';
                    for (var i = 0; i < cfeatures.length; i++) {
                        $(popup_content).append('<article><strong>' + cfeatures[i].get('name') + '</article>');
                if (cfeatures.length == 1) {
                    popup_content.innerHTML = '<h5><strong>' + cfeatures[0].get('name') + '</strong></h5><i>this is a single, but <b>clustered</b> feature</i>';
        } else {
map.on('click', OpenPopup);