Skip to main content

Dynamic width and multi-segment dashes

Shared 2D line-dasharray accepts 1–16 nonnegative numbers, alternating painted segments and gaps in multiples of the current line width. [4, 2, 1, 2] creates a long dash, gap, short dash and gap: 16, 8, 4 and 8 CSS pixels at a 4px width. An odd array ends and starts with painted segments that join at the cycle boundary. [3, 1, 1] therefore produces a four-width dash and a one-width gap. The next cycle is not inverted as in Canvas.

map.addSource('roads', { type: 'geojson', data: {
  type: 'Feature', id: 1, properties: { width: 4, dash: [4, 2, 1, 2] },
  geometry: { type: 'LineString', coordinates: [[-10, 0], [10, 0]] }
} });
map.addLayer({ id: 'road-dash', type: 'line', source: 'roads', paint: {
  'line-color': 'blue', 'line-width': ['get', 'width'],
  'line-dasharray': ['get', 'dash']
} });
await map.sqs.flush({ render: true });
map.setPaintProperty('road-dash', 'line-dasharray',
  ['coalesce', ['feature-state', 'dash'], ['literal', [4, 2, 1, 2]]]);
map.setFeatureState({ source: 'roads', id: 1 }, { dash: [1, 1, 3, 1] });
await map.sqs.flush({ render: true });

Use get, feature-state, global-state, lexical variables and shared case, match, coalesce or step expressions. Wrap constant branch arrays in ['literal', [...]]. Dash zoom uses integer values; width retains its existing paint-expression evaluation. Source/state updates preserve layer identity, and phase continues across line vertices.

The 16-entry limit is a project budget, not a MapLibre specification limit. Zero entries are allowed but the total cycle must be positive. Each entry times the resolved width must be at most 100 CSS pixels; dynamic widths retain their 24px maximum preflight. Empty, negative, nonnumeric, all-zero, oversized and over-budget arrays are rejected without truncation. Failed source/style candidates preserve committed content.

commonLineDashArrays reports this shared 2D subset. Full pitched/globe dashes, cap/join options and querying individual gap pixels remain unavailable. Queries retain the whole-line envelope. Applications create their own layer/tool UI.

The complete portable script targets native persistent QuickJS, browser main-thread and map Worker hosts. Standalone SDK callers replace map.sqs.flush() with their map's flush(). Pixel and platform evidence is recorded separately; phone/device acceptance and formal releases remain pending.