This example uses OpenLayers v10.9.0. The latest is v.
This example uses features that are not part of the stable API and subject to change between releases. Consult the API documentation to see what is supported in the latest release.
Edit

#### GeoZarr Multi-Group

zarr 4 geozarr 4 sentinel-2 3 cloud 1 masking 1

#####

Cloud threshold: 50%

Cloud-masked Sentinel-2 imagery using bands from multiple Zarr groups.

The GeoZarr source supports loading bands from multiple groups within the same Zarr hierarchy. In this example, a true-color composite is rendered from B02 (blue), B03 (green), and B04 (red) reflectance values from the `measurements/reflectance` group. The cloud probability (`cld`) from the `quality/probability` group is used to fade out cloudy areas, revealing the OSM basemap underneath.

When bands come from different groups, each band is specified as an object with a `name` and a `group` path. The `group` is resolved relative to the `url`, which can be any common ancestor in the Zarr hierarchy — not necessarily the store root.

Since the cloud probability data is only available at 20 m resolution, it is resampled at higher zoom levels to match the 10 m reflectance bands.

##### main.js

    import Map from 'ol/Map.js';
    import {getView, withExtentCenter, withHigherResolutions} from 'ol/View.js';
    import Link from 'ol/interaction/Link.js';
    import TileLayer from 'ol/layer/WebGLTile.js';
    import GeoZarr from 'ol/source/GeoZarr.js';
    import OSM from 'ol/source/OSM.js';

    const source = new GeoZarr({
      url: 'https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260120T125339_N0511_R138_T27VWL_20260120T131151.zarr',
      bands: [
        {name: 'b04', group: 'measurements/reflectance'},
        {name: 'b03', group: 'measurements/reflectance'},
        {name: 'b02', group: 'measurements/reflectance'},
        {name: 'cld', group: 'quality/probability'},
      ],
    });

    const layer = new TileLayer({
      style: {
        variables: {threshold: 50},
        gamma: 1.5,
        color: [
          'color',
          ['interpolate', ['linear'], ['band', 1], 0, 0, 0.5, 255],
          ['interpolate', ['linear'], ['band', 2], 0, 0, 0.5, 255],
          ['interpolate', ['linear'], ['band', 3], 0, 0, 0.5, 255],
          // Hide pixels whose cloud probability exceeds the threshold.
          ['case', ['>', ['band', 4], ['var', 'threshold']], 0, 1],
        ],
      },
      source,
    });

    const map = new Map({
      layers: [
        new TileLayer({
          source: new OSM(),
        }),
        layer,
      ],
      target: 'map',
      view: getView(source, withHigherResolutions(2), withExtentCenter()),
    });

    map.addInteraction(new Link());

    const thresholdSlider = document.getElementById('threshold');
    const thresholdValue = document.getElementById('threshold-value');
    thresholdSlider.addEventListener('input', function () {
      thresholdValue.textContent = thresholdSlider.value;
      layer.updateStyleVariables({threshold: parseFloat(thresholdSlider.value)});
    });

##### index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>GeoZarr Multi-Group</title>
        <link rel="stylesheet" href="node_modules/ol/ol.css">
        <style>
          .map {
            width: 100%;
            height: 400px;
          }
        </style>
      </head>
      <body>
        <div id="map" class="map"></div>
        <div class="row mt-2">
          <div class="col-auto">
            <label class="form-label" for="threshold">Cloud threshold: <span id="threshold-value">50</span>%</label>
            <input type="range" class="form-range" id="threshold" min="0" max="100" value="50">
          </div>
        </div>

        <script type="module" src="main.js"></script>
      </body>
    </html>

##### package.json

    {
      "name": "geozarr-groups",
      "dependencies": {
        "ol": "10.9.0"
      },
      "devDependencies": {
        "vite": "^3.2.3"
      },
      "scripts": {
        "start": "vite",
        "build": "vite build"
      }
    }