UI & Layout¶
The ui block in your project's settings.json adjusts the shell: panel sides and widths, the top bar, the layer list, the info modal, the loading overlay, mobile sizing and more. Every sub-block is optional; leave one out to use its defaults.
Sizes are written in px, measured at the browser's default font size (16px). Icon sizes, logo sizes and the top-bar height grow and shrink with the user's font-size setting, like the text does. Panel widths stay fixed, so a larger font fits fewer words per line in a panel but doesn't take more of the map.
"ui": {
"layout": {},
"topBar": {},
"floatingControls": {},
"layerList": {},
"info": {},
"loader": {},
"widgetHeader": {},
"layerDefaults": {},
"mobile": {},
"textSizes": {},
"hideWidgetInfo": [],
"buttonCursor": "pointer",
"popups": {}
}
ui.popups has its own page, Popups. Everything else is below.
Layout: ui.layout¶
Sets which side of the desktop layout each region sits on, the side rail's sizes, and which parts of the shell show. These are desktop only; the mobile layout ignores them.
| Field | Type | Default | Description |
|---|---|---|---|
sideRailSide |
"left" \| "right" |
"left" |
Side of the side rail (icon strip and slide-out panel) |
topBarPanelSide |
"left" \| "right" |
opposite sideRailSide |
Side a top-bar widget's panel opens on. Only needed when the side rail has no widgets |
topBarWidgetsSide |
"left" \| "right" |
"right" |
Side of the top-bar widget buttons. The logo, title and info button take the other side |
showInfo |
boolean |
true |
Show the info (ⓘ) button in the top bar |
showBranding |
boolean |
true |
Show the logo and title |
showTopBar |
boolean |
true |
Show the top bar, with its title, logo, info button and widgets |
showSideRail |
boolean |
true |
Show the side rail and its widgets |
sideRailOpenOnLoad |
string |
closed | A side panel widget's id (e.g. "layer-list") whose panel is open when the app loads |
sideRailPanelWidth |
number |
288 |
Width (px) of the side rail's slide-out panel |
sideRailStripWidth |
number |
48 |
Width (px) of the side rail's icon strip, which is always visible |
sideRailIconSize |
number |
20 |
Size (px) of the side-rail widget icons. Each button is the icon plus 12px of padding |
sideRailChevronSize |
number |
16 |
Size (px) of the side panel's collapse chevron |
The strip is 48px wide by default, so icons above about 36px crowd it. To fit bigger icons, raise sideRailStripWidth past 48.
A top-bar widget's panel opens opposite the side rail, so the two never overlap. When both the side rail and the top bar have widgets, setting topBarPanelSide to the rail's side stops the app at boot with an error that explains the fix. With an empty side rail, it can open on either side.
showTopBar and showSideRail only hide the bar on desktop; the mobile layout keeps its own bar and still offers every widget in its carousel. Widgets placed on a hidden bar are skipped on desktop, with a note in the browser console ([conness-gis] …). A sideRailOpenOnLoad id that isn't in sidePanelWidgets logs a warning and the panel starts closed.
Top bar: ui.topBar¶
On narrow screens the title shortens with an ellipsis (hover shows it in full), so the widget buttons always stay on screen.
| Field | Type | Default | Description |
|---|---|---|---|
height |
number |
56 |
Top-bar height in pixels; the map resizes to fit |
iconSize |
number |
20 |
Size (px) of the widget icons. Each button is twice the icon |
mobileIconSize |
number |
iconSize |
Icon size in the mobile layout (the carousel) |
logoSize |
number |
32 |
Logo height (px). The width follows the image's proportions, so a wide logo isn't squashed |
mobileLogoSize |
number |
logoSize, then 28 |
Logo height in the mobile layout |
titleSize |
number |
18 |
The app title's text size (px) |
mobileTitleSize |
number |
16 |
The app title's text size in the mobile layout |
mobileVisibleCount |
number |
3 |
How many widget buttons the mobile carousel shows at once |
mobileCarouselMode |
"step" \| "paged" |
"step" |
"step" moves one widget per arrow click; "paged" moves a full window |
shadow |
object | none | Shadow cast onto the map (below) |
The bar has three separate sizes: height for the bar, iconSize for the widget icons and their buttons, and logoSize for the logo. Nothing caps the last two, so a large icon or logo can overflow the bar; raise height along with them. An SVG logo needs a viewBox, or its width won't scale.
Top-bar shadow¶
A light shadow under the top bar. It only falls over the map, never the side panels, so it stays put when a panel slides out.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
false |
Turn the shadow on |
strength |
number (0 to 1) |
0.4 |
Scales both darkness and height. Try 0.2 for a faint shadow, 0.7 for a strong one |
Floating controls: ui.floatingControls¶
Sizes the floating widget buttons and MapLibre's built-in controls (zoom, geolocate, globe, …) together, so the two sets match.
| Field | Type | Default | Description |
|---|---|---|---|
iconSize |
number |
25 |
Icon size (px). The icon fills 75% of its button, so the default button is about 33px |
mobileIconSize |
number |
iconSize |
Icon size in the mobile layout |
Layer list: ui.layerList¶
Defaults for the Layer List widget.
| Field | Type | Default | Description |
|---|---|---|---|
groupsCollapsedByDefault |
boolean |
false |
Whether layer groups start collapsed |
toggleStyle |
"eye" \| "checkbox" |
"eye" |
The control that shows and hides layers |
actions |
object: opacity, labels, zoomToExtent, filter, export (each boolean) |
all on | Which items appear in each row's menu. Labels only appears on layers with a label config, Filter only on layers that declare filters, and Export Data only on geojson and geoparquet layers |
rowClickToggle |
boolean |
true |
A click anywhere on a layer row toggles the layer, not only a click on the eye or checkbox. Group headers aren't affected |
exportFormats |
list of "geojson", "kml", "geoparquet", "csv" |
all four | Formats Export Data offers, in this order. With one, Export Data downloads it directly instead of asking. An empty list means all |
To set collapse per group, use the top-level layerGroups block, keyed by group name:
Info modal: ui.info¶
The "About this app" modal shows your project's info.md. By default it only opens from the top bar's ⓘ button. These settings make it open on load and set its size. The markdown renderer is downloaded the first time the modal opens. The page's first # heading is its title, with a line under it.
| Field | Type | Default | Description |
|---|---|---|---|
autoOpen |
boolean |
false |
Open the modal on load |
allowDismiss |
boolean |
false |
With autoOpen, show a "don't show again" checkbox. The choice is tied to the content of info.md, so editing the file shows it to returning users again |
width |
number |
672 |
Widest the modal gets (px). Narrow screens use their own width |
maxHeight |
number |
80 |
Tallest the modal gets, as a percentage of the screen height. Longer content scrolls |
With autoOpen on, closing the modal (X, Escape or a click outside) only lasts until the next load. Only the "don't show again" checkbox stops it opening on load. Without autoOpen the checkbox never shows, since the modal only opens when someone asks for it.
Loading overlay: ui.loader¶
An overlay shown at startup while layer data loads. It dims the app and blocks clicks, so nobody opens a widget mid-load. The spinner uses the primary color.
It waits for every layer, including ones hidden at startup: hidden data layers still load at boot, and the map would stay busy for a few seconds if the overlay closed before them. So the map is ready to use the moment the overlay fades. To skip hidden layers at startup and load them when they're first turned on, set map.onlyVisibleOnLoad; then the overlay doesn't wait for them. With no layers at all, it clears as soon as the basemap loads. Once startup loading finishes, the overlay doesn't come back, even after a basemap switch.
Under the label, one line says what's happening: DuckDB's startup steps while they run ("DuckDB", then each extension), then the name of a layer that's still loading, which changes as layers finish.
"loader": {
"label": ["Loading the map…", "Fetching layers…", "Almost there…"],
"labelInterval": 2500,
"showDetails": true
}
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Show the overlay |
label |
string or a list of strings |
none | Text under the spinner, e.g. "Loading…". A list shows one line at a time, looping, with a short fade between lines |
labelInterval |
number |
2500 |
With a label list, how long each line shows, in ms |
showDetails |
boolean |
true |
Show the line under the label naming what's loading |
Widget header: ui.widgetHeader¶
The title bar at the top of every widget panel: the widget's name, a close X and an optional divider.
| Field | Type | Default | Description |
|---|---|---|---|
showTitle |
boolean |
true |
Show the widget's name |
showDivider |
boolean |
true |
Show the line under the title |
These apply to every widget, and each placement can override them (see Building a Custom Widget). The title color is a theme setting, widgetTitleColor in your project's theme.json; see Theme.
Widget info sections: ui.hideWidgetInfo¶
Some widgets (Bookmarks, Draw, Measure) open with a short info section explaining how to use them. List a widget's id here to hide its info section:
Text sizes: ui.textSizes¶
All of the shell's and the widgets' text uses one of these sizes, so changing one changes every piece of text that uses it. Set in px; like the other sizes, they still follow the browser's font size.
| Field | Default | Used for |
|---|---|---|
small |
11 |
Fine print: coordinates, opacity percentages, badges, hints |
medium |
12 |
Layer names in the layer list, captions, widgets' info sections, and any text without its own size |
large |
14 |
Layer group names, list rows, menus, inputs, the info modal and the loading screen |
title |
14 |
Widget and side panel titles |
button |
14 |
Buttons inside widgets |
dialogButton |
12 |
Cancel and confirm buttons in dialogs |
The app title has its own size, ui.topBar.titleSize, and popups have three of their own on the Popups page. Map labels are sized per layer or in ui.layerDefaults.
Global layer defaults: ui.layerDefaults¶
Set a default once for every layer instead of repeating it on each. A layer's own values always win. For now this covers label styling, and only affects layers that have labels.
"layerDefaults": {
"label": { "bold": true, "fontSize": 12, "color": "#333333", "haloColor": "#ffffff", "haloWidth": 1 }
}
The fields match a layer's label config, minus field, expression and minZoom, which stay per layer. Without it, labels use Noto Sans Regular at 12px in #333 with a white halo; "bold": true switches to Noto Sans Bold.
Mobile: ui.mobile¶
The mobile layout is used below 500 px wide, which means portrait phones. Landscape phones, tablets and anything wider get the desktop layout. Turning a phone or resizing a window across 500 px swaps the layout and leaves the map as it was: same view, basemap, layers and filters. The mobile layout fits screens down to 320 px wide.
ui.mobile sets the heights of the mobile bottom sheet, as a percentage of screen height (vh).
| Field | Type | Default | Description |
|---|---|---|---|
initialSheetVh |
number |
25 |
Height when the sheet first opens |
minSheetVh |
number |
12 |
Smallest the user can drag it to |
maxSheetVh |
number |
65 |
Largest the user can drag it to |
Button cursor: ui.buttonCursor¶
| Value | Effect |
|---|---|
"pointer" (default) |
The hand cursor over every button: widgets, popups, panel toggles and map controls |
"default" |
Buttons keep the normal arrow, which feels more like a desktop app |
Right-click context menu: contextMenu¶
A top-level block, not under ui. It sets up the right-click menu, which is desktop only. The menu opens toward the middle of the map, so it never runs off the edge. Set a built-in item to true to include it; custom items come last. Leave out the whole block, or set "enabled": false, to get the browser's own right-click menu back. enabled keeps your items for when you turn it on again.
"contextMenu": {
"enabled": true,
"centerHere": true,
"googleMaps": true,
"googleStreetView": true,
"custom": [
{ "label": "Open in Bing Maps", "url": "https://www.bing.com/maps?cp={lat}~{lng}&lvl={zoom}" }
]
}
osm, appleMaps and copernicus are the other built-in items.
| Field | Type | Description |
|---|---|---|
enabled |
boolean |
Default true. false turns the menu off and gives the browser's menu back |
centerHere |
boolean |
Fly the map to the clicked point |
googleMaps / googleStreetView / osm / appleMaps / copernicus |
boolean |
"Open in …": opens that site in a new tab at the clicked point |
custom |
list of { "label", "url" } |
Your own items. url can use {lat}, {lng} and {zoom} placeholders. {lng} is always between -180 and 180, even after panning across the date line |