View on GitHub Sample viewer app

Display a map view that updates between authored light, dark, and high-contrast basemaps.

Image of Update basemap for contrast accessibility

Use case

Use this pattern when your app needs contrast-responsive basemaps to switch between light, dark, and high-contrast states. This is especially useful with basemaps authored for accessibility, along with reference layers for the associated base layer.

How to use the sample

When the sample is launched, it displays the chosen contrast basemap. When automatic mode is selected for the sample, changing the device appearance in the device system settings between light and dark theme, or turn high contrast on and off, will result in the appropriate basemap being loaded to match settings. Toggle the device settings to see the different basemaps.

Switch to manual mode to choose Light, Dark, High contrast light, or High contrast dark directly. Show or hide the basemap’s reference layers to compare how labels and boundaries read in each contrast appearance mode.

How it works

  1. Provide four authored basemaps that represent the supported contrast appearances: Light, Dark, High contrast light, and High contrast dark.
  2. Resolve which contrast appearance should be active based on the current mode and device settings.
    • In manual mode, use the appearance selected in the supporting pane.
    • In automatic mode, resolve the appearance from the device’s current light, dark, and high-contrast settings. This sample uses a custom rememberDeviceContrastSettings() Composable.
  3. Map the resolved appearance to an ArcGIS Online Basemap or a BasemapStyle and update the map’s basemap.
  4. Apply the current reference-layer visibility setting to the basemap’s labels and boundary layers.

Relevant API

  • Basemap
  • BasemapStyle
  • Map

About the data

This sample uses four ArcGIS Living Atlas web maps authored for regular light, regular dark, high-contrast light, and high-contrast dark presentation states.

The enhanced contrast web maps are designed for accessibility-focused presentation workflows, and the light and dark canvas maps provide the regular contrast companions. You can use these web maps as a starting reference for your own contrast-specific basemap workflows.

Additional information

For more background information on the cartographic approach behind the enhanced contrast basemaps, see Working with Enhanced Contrast basemaps to improve accessibility.

On Android, automatic mode responds to system light and dark theme changes and to high-contrast settings. Android 14 and later uses UiModeManager, while earlier versions read the accessibility high-text-contrast setting.

Tags

accessibility, accessible, basemap, colorblind, contrast, dark, enhanced, high, inclusive, legibility, light, living atlas, readability, vision, visual impairment, WCAG

Sample code

UpdateBasemapForContrastAccessibilityViewModel.kt UpdateBasemapForContrastAccessibilityViewModel.kt MainActivity.kt UpdateBasemapForContrastAccessibilitySupportingPane.kt UpdateBasemapForContrastAccessibilityScreen.kt DeviceContrastSettings.kt
/* Copyright 2026 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.esri.arcgismaps.sample.updatebasemapforcontrastaccessibility.components
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.Basemap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
* ViewModel for the UpdateBasemapForContrastAccessibility sample.
*
* Owns the selected effective contrast appearance, to keep the map synchronized to the contrast basemap type.
*/
class UpdateBasemapForContrastAccessibilityViewModel(app: Application) : AndroidViewModel(app) {
val arcGISMap = ArcGISMap(spatialReference = SpatialReference.webMercator()).apply {
initialViewpoint = Viewpoint(34.05, -117.19, 2e6)
}
private val _contrastUiState = MutableStateFlow(ContrastUiState.defaultState)
val contrastUiState = _contrastUiState.asStateFlow()
val messageDialogVM = MessageDialogViewModel()
/**
* Ensures the selected [contrast] is in sync with the MapView.
*/
fun syncContrast(contrast: ContrastAppearance) {
if (_contrastUiState.value.contrastAppearance == contrast) return
applyContrastBasemap(contrast)
}
/**
* Applies the contrast-specific basemap to the MapView.
*/
private fun applyContrastBasemap(contrast: ContrastAppearance) {
updateContrastAppearance(contrast = contrast)
val isVisible = _contrastUiState.value.isReferenceLayersEnabled
arcGISMap.setBasemap(contrastBasemapFor(contrast))
viewModelScope.launch {
arcGISMap.load().getOrElse { messageDialogVM.showMessageDialog(it) }
applyReferenceLayersVisibility(map = arcGISMap, isVisible = isVisible)
}
}
/**
* Applies the current reference-layers [isVisible] flag to the [map].
*/
private fun applyReferenceLayersVisibility(map: ArcGISMap, isVisible: Boolean) {
map.basemap.value?.referenceLayers?.forEach { layer ->
layer.isVisible = isVisible
}
}
/**
* Updates whether the sample resolves the [mode] automatically or uses the manual picker.
*/
fun updateContrastMode(mode: ContrastMode) {
_contrastUiState.update { currentState ->
currentState.copy(contrastMode = mode)
}
}
/**
* Updates the [contrast] appearance while the sample is in manual mode.
*/
fun updateContrastAppearance(contrast: ContrastAppearance) {
_contrastUiState.update { currentState ->
currentState.copy(contrastAppearance = contrast)
}
}
/**
* Update reference layers using [isVisible].
*/
fun updateReferenceLayerVisibility(isVisible: Boolean) {
_contrastUiState.update { currentState ->
currentState.copy(isReferenceLayersEnabled = isVisible)
}
applyReferenceLayersVisibility(map = arcGISMap, isVisible = isVisible)
}
}
/**
* UI states for the controls in the supporting pane to configure the displayed MapView.
*/
data class ContrastUiState(
val contrastMode: ContrastMode,
val contrastAppearance: ContrastAppearance,
val isReferenceLayersEnabled: Boolean
) {
companion object {
val defaultState = ContrastUiState(
contrastMode = ContrastMode.Automatic,
contrastAppearance = ContrastAppearance.HighContrastLight,
isReferenceLayersEnabled = true
)
}
}
/**
* State to track whether appearance comes from device settings or the manual picker.
*/
enum class ContrastMode {
Automatic,
Manual
}
/**
* State to track the four contrast appearance variants.
*/
enum class ContrastAppearance {
Light,
HighContrastLight,
Dark,
HighContrastDark
}
/**
* Maps the selected appearance to the contrast accessibility basemaps used by the sample.
*/
private fun contrastBasemapFor(contrast: ContrastAppearance): Basemap {
return when (contrast) {
ContrastAppearance.Light -> Basemap(BasemapStyle.ArcGISLightGray)
ContrastAppearance.Dark -> Basemap(BasemapStyle.ArcGISDarkGray)
ContrastAppearance.HighContrastLight -> Basemap("https://www.arcgis.com/home/item.html?id=084291b0ecad4588b8c8853898d72445")
ContrastAppearance.HighContrastDark -> Basemap("https://www.arcgis.com/home/item.html?id=3e23478909194c54992eaaee78b5f754")
}
}