✏️ Drawing
What You'll Master in This Chapter
- Drawing Fundamentals: Master Google's DrawingManager, then migrate to modern Terra Draw
- Custom Freehand Drawing: Advanced techniques that rival Zillow, Airbnb, and Compass
- Mobile Touch Support: Seamless drawing across all devices with optimized touch events
Once you've grasped the concepts behind 🐉 Dragging & Editing , you can delight your users by letting them draw on your Google map.
Drawing on the map canvas enables a variety of use cases. Your users may want to:
- Emphasize custom region(s) by drawing polygons, just like on Garages Near Me .
- “Clip” neighborhoods or buildings out of the basemap, just like with Felt .
- Restrict area(s) of interest by freehand drawing, just like on Zillow or Compass .
As a matter of fact, being able to draw as part of an advanced search is one of the top community feature requests at Airbnb:
Original tweet link
Now, implementing user-friendly map drawing capabilities is daunting but not impossible.
In this chapter you'll learn how to A) work with Google Maps' Drawing Library (and migrate to Terra Draw), and B) implement a custom, self-closing, freehand drawing tool.
Important update: Google's Drawing Library was deprecated in August 2025 and will be removed in May 2026. You might see a console warning like this:
Drawing library functionality in the Maps JavaScript API is deprecated. This API was deprecated in August 2025 and will be made unavailable in a later version of the Maps JavaScript API, releasing in May 2026. For more info, see https://developers.google.com/maps/deprecations.
Existing implementations have until May 2026 to migrate. Do reach out if you need help with the migration.
While we'll teach you how the Drawing Library works (great for understanding drawing concepts), we recommend Terra Draw for new projects.
To use Google's Drawing Library (i.e. the google.maps.drawing.DrawingManager class), you'll first need to load this library as part of your initialization script :
<script
async
defer
src=".../maps/api/js?key=YOUR_KEY_STARTING_WITH_AIza&libraries=drawing&callback=initMap"
></script> From there, you can create a DrawingManager instance that'll look like this by default :
The default MARKER, CIRCLE, POLYGON, POLYLINE, and RECTANGLE drawing modes.
To limit the drawing modes to e.g. only polygons, specify the drawingModes parameter .
To define the initial drawing state of the manager, set the drawingMode parameter .
const drawingManager = new google.maps.drawing.DrawingManager({
map,
drawingControlOptions: {
drawingModes: [google.maps.drawing.OverlayType.POLYGON],
},
drawingMode: google.maps.drawing.OverlayType.POLYGON
polygonOptions: {
editable: true,
draggable: true,
},
})); The drawback of the default drawing manager is that it is not easily customizable — the buttons are too small, there's no “delete” button, etc.
The good news is, the drawing manager's functionality can be reused to create aesthetically pleasing and functional drawing tools:
Let's break that down.
Our pencil button will be a traditional HTML <button> element
<button id="pencil"></button> #pencil {
position: absolute;
appearance: none;
border: none;
border-radius: 2px;
width: 36px;
height: 36px;
background: white;
cursor: pointer;
box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px;
background-position: center center;
background-size: 22px;
background-repeat: no-repeat;
top: 12px;
right: 12px;
} whose background is the encoded pencil SVG:
#pencil {
background-image: url("data:image/svg+xml,%3Csvg width='34' height='34' viewBox='0 0 34 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='22.2622' width='12.8898' height='3.50367' rx='1' transform='rotate(43.322 22.2622 0)' fill='%23AC1717'/%3E%3Cpath d='M4.57277 21.7427L2.85168 30.8399C2.6367 31.9763 3.73422 32.9181 4.82477 32.5332L13.1844 29.5828C14.2488 29.2071 14.5325 27.8346 13.7042 27.0677L7.06574 20.9209C6.20058 20.1199 4.79196 20.5842 4.57277 21.7427Z' fill='%23E7C3B4' stroke='%23E3B495'/%3E%3Cpath d='M4.89312 20.1657L5.28403 20.4758L17.841 4.64311C18.0194 4.41812 18.3503 4.38932 18.5649 4.58011L27.5472 12.5644C27.7518 12.7462 27.7721 13.0587 27.5929 13.2656L14.4498 28.4307C14.272 28.6358 13.9349 28.5101 13.9349 28.2386C13.9349 27.5244 13.3559 26.9454 12.6417 26.9454H10.1945C9.95004 26.9454 9.74145 26.7687 9.70126 26.5276L9.39585 24.6951C9.27531 23.9718 8.64952 23.4417 7.91626 23.4417H5.67578C5.39963 23.4417 5.17578 23.2179 5.17578 22.9417V20.7865C5.17578 20.6737 5.21393 20.5642 5.28403 20.4758L4.89312 20.1657Z' fill='%23F5AF00' stroke='%23EEA502'/%3E%3Cpath d='M2.07456 33.6272L2.88261 29.587C2.92206 29.3897 3.19534 29.3676 3.26599 29.556L3.99754 31.5068C4.02365 31.5764 4.08622 31.6258 4.16 31.635L6.02763 31.8685C6.23008 31.8938 6.26914 32.1704 6.08161 32.2507L2.34946 33.8502C2.20145 33.9137 2.04298 33.7851 2.07456 33.6272Z' fill='%23696969' stroke='%234D4D4D' stroke-width='0.6'/%3E%3Cpath shape-rendering='geometricPrecision' d='M7.1 23.8L21.38 6.8' stroke='%23EBA000'/%3E%3Cpath shape-rendering='geometricPrecision' d='M10.5 27.2L25.46 10.2' stroke='%23EBA000'/%3E%3Crect x='19.9264' y='1.16789' width='15.1826' height='3.50367' rx='1' transform='rotate(43.322 19.9264 1.16789)' fill='%23D9D9D9'/%3E%3C/svg%3E%0A");
} This SVG icon, when converted to a PNG , can be used as a mouse cursor image when the user hovers their mouse over the map canvas:
#map.will-draw {
/* Though the pencil image is 32x32px, safari won't render the cursor position correctly
unless it's ever-so-slightly less than that.
See https://stackoverflow.com/a/35243428/8160318 */
cursor:
url(https://.../pencil.png) 3 31,
crosshair !important;
} Speaking from personal experience, using SVGs as cursor images isn't recommended because … they don't work properly across browsers, esp. in Safari on MacOS.
In reality, the CSS selector will be a bit more complicated than just #map.will-draw:
#map.will-draw .gm-style div + div,
#map.will-draw .gm-style div div div div {
cursor:
url(https://.../pencil.png) 3 31,
crosshair !important;
} Does the .gm-style class look familiar? We talked about it in 🏷️ Custom Info Windows .
Now, the trick is to dynamically toggle the .will-draw class when the pencil button is clicked:
const toggleMapCls = () => {
document.getElementById("map").classList.toggle("will-draw");
};
const handlePencilClick = () => {
toggleMapCls();
// more logic later on ...
};
document.getElementById("pencil").addEventListener("click", handlePencilClick); Once the “pencil” button is set up, we can move onto the “trash” button.
The idea is to:
- Keep showing the “pencil” button while the user draws their polygon.
- Hide the “pencil” button after the polygon is finished and show the “trash” button.
- Once the “trash” button is clicked, the polygon is removed and the “pencil” button reappears.
<button id="trash" hidden></button> #pencil,
#trash {
position: absolute;
appearance: none;
border: none;
border-radius: 2px;
width: 36px;
height: 36px;
background: white;
cursor: pointer;
box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px;
background-position: center center;
background-size: 22px;
background-repeat: no-repeat;
top: 12px;
right: 12px;
} #trash {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' stroke='none' stroke-linecap='round' stroke-linejoin='round' fill='none' shape-rendering='geometricPrecision' preserveAspectRatio='xMidYMid meet' style='vertical-align: top;'%3E%3Cpath fill='%23FF0000' d='m1.65139,6.0885l0,-1.47806a1.47806,1.47806 0 0 1 1.47806,-1.47806l5.90927,0a2.95611,2.95611 0 1 1 5.91222,0l5.91961,0a1.47806,1.47806 0 0 1 1.47806,1.47806l0,1.47806l-20.69721,0zm2.21413,2.95611l16.25861,0l-0.66808,13.3764a1.47806,1.47806 0 0 1 -1.47806,1.40415l-11.96634,0a1.47806,1.47806 0 0 1 -1.47658,-1.40415l-0.66956,-13.3764z'/%3E%3C/svg%3E");
} To keep the toggling in sync, we can swap the elements' hidden attribute :
const toggleTrashVisibility = () => {
document.getElementById("trash").hidden =
!document.getElementById("trash").hidden;
};
const togglePencilVisibility = () => {
document.getElementById("pencil").hidden =
!document.getElementById("pencil").hidden;
};
const handleTrashClick = () => {
toggleTrashVisibility();
togglePencilVisibility();
// more logic later on ...
};
document.getElementById("trash").addEventListener("click", handleTrashClick); Now comes the fun part. We can instantiate the DrawingManager and hide it from the UI by setting the drawingControl property to false:
const initPencilDrawing = () => {
const drawingManager = (window.drawingManager =
new google.maps.drawing.DrawingManager({
map: window.map,
drawingMode: google.maps.drawing.OverlayType.POLYGON,
drawingControl: false,
polygonOptions: {
editable: true,
draggable: true,
},
}));
drawingManager.addListener("polygoncomplete", handlePolygonComplete);
}; Notice that we're storing the drawingManager on the global window object for easier access throughout the app. To safely access this foreign prop in Typescript, we'll declare a global interface and store the map and drawnPolygon for later use as well:
declare global {
interface Window {
map?: google.maps.Map;
drawingManager?: google.maps.drawing.DrawingManager;
drawnPolygon?: google.maps.Polygon;
}
} Finally, we'll attach a handler to be notified when the polygon drawing was completed :
drawingManager.addListener("polygoncomplete", handlePolygonComplete); In this handler, we'll:
- Turn off the “pencil” cursor.
- Toggle the “trash” and “pencil” buttons.
- Store the
drawnPolygon. - Delete the
drawingManagerbecause we don't need it anymore.
const handlePolygonComplete = (drawnPolygon: google.maps.Polygon) => {
toggleMapCls();
toggleTrashVisibility();
togglePencilVisibility();
window.drawnPolygon = drawnPolygon;
window.drawingManager.setMap(null);
window.drawingManager = undefined;
// more logic later on ...
}; Note that we've used .setMap(null) on the drawing manager — just like we did when we talked about removing artifacts from the map.
So far so good. We've got a handler that'll be fired when the polygon is completed. How do we listen to polygon adjustments and dragging?
We'll recycle the knowledge from the previous chapter , including debouncing :
import { debounce } from './debounce';
export const detectChanges = (
poly: google.maps.Polygon,
onChange: Function
) => {
poly.getPaths().forEach(function (path) {
['insert_at', 'remove_at', 'set_at'].map((evt) => {
path.addListener(
evt,
debounce(() => {
onChange();
}, 150)
);
});
});
};
const handlePolygonComplete = (drawnPolygon: google.maps.Polygon) => {
...
detectChanges(window.drawnPolygon, triggerSearch);
...
}; This detection mechanism will cover changes to the polygon's vertices as well as to arbitrary dragging of the polygon artifact.
Assuming this example powers a search feature, we'll want to “trigger the search” when:
- The “trash” button is clicked → the polygon has been flushed.
- Any time a new polygon is drawn.
- Any time an existing polygon is modified or dragged.
To achieve these goals, we'll go with:
const handleTrashClick = () => {
...
triggerSearch();
};
const handlePolygonComplete = (drawnPolygon: google.maps.Polygon) => {
...
triggerSearch();
detectChanges(window.drawnPolygon, triggerSearch);
};
const triggerSearch = () => {
console.info('Will trigger search', {
polygonAsGeoJSON: serialize(window.drawnPolygon),
});
}; Notice the serialize function in the console.info call. You'll learn more about GeoJSON & about serializing polygon coordinates in the next chapters of this book.
Here's a working demo in vanilla Typescript:
Now that you understand how DrawingManager works, here's how to achieve the same result with Terra Draw:
Migration Path: With Google's Drawing Library deprecated , Terra Draw offers the same functionality, plus straightforward GeoJSON output and better mobile support.
// Instead of DrawingManager (deprecated)
const drawingManager = new google.maps.drawing.DrawingManager({
map,
drawingMode: google.maps.drawing.OverlayType.POLYGON,
polygonOptions: { editable: true, draggable: true },
}); // Use Terra Draw
import { TerraDraw, TerraDrawPolygonMode, TerraDrawSelectMode } from 'terra-draw';
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';
export const initMap = () => {
const map = new google.maps.Map(mapContainer, {
...
});
google.maps.event.addListenerOnce(map, 'projection_changed', () => {
const draw = new TerraDraw({
adapter: new TerraDrawGoogleMapsAdapter({
map,
lib: google.maps
}),
modes: [
new TerraDrawPolygonMode(),
new TerraDrawSelectMode({
flags: {
polygon: {
feature: {
draggable: true,
coordinates: {
editable: true
}
}
}
}
}),
],
});
draw.start();
draw.setMode('polygon');
// Same event pattern, just different syntax
draw.on('finish', (id, geometry) => {
console.log('Polygon completed:', geometry); // GeoJSON format
});
});
}; I'm running a real estate search website and want to let my users draw on the map with a freehand “pencil tool.” The drawing should produce a polygon and self-close if the starting & ending points aren't equal.
Screen recording courtesy of Garages Near Me . The logic is heavily inspired by this SO answer .
Google Maps' native DrawingManager doesn't support freehand drawing so we'll write our own manager.
To start off, we can reuse the “pencil” cursor and “pencil/trash” buttons from above .
From there, we'll need to think about what needs to happen when the “pencil” button is pressed:
- We'll want to track when the mouse/trackpad is pressed by listening to an initial
mousedownDOM event on the map'sdiv. This will trigger the drawing mode. - Next, we'll create a
Polyline. The polyline'spathwill be continuously updated by listening to the map'smousemoveevent . - Then, we'll track when the mouse/trackpad is released by listening to the map
div'smouseupevent . - Finally, we'll extract the polyline's path and use it to initialize the final polygon.
During the drawing process, the user might accidentally drag the map or click on the POIs .
To prevent this from happening, we'll disable the applicable options :
export const toggleMapInteractivity = (isOn: boolean) => {
window.map.setOptions({
draggable: isOn,
zoomControl: isOn,
scrollwheel: isOn,
disableDoubleClickZoom: isOn,
clickableIcons: isOn,
});
}; To listen to the mousedown DOM event, you may be tempted to use google.maps.event.addDomListenerOnce . Bear in mind that this method is now deprecated and you should use .addEventListener instead :
const initPencilDrawing = () => {
toggleMapInteractivity(false);
window.map.getDiv().addEventListener(
"mousedown",
() => {
drawFreehand(onDrawingComplete);
},
{ once: true }, // <-- one-time listener
);
}; In the the drawFreehand function, we'll initialize a non-clickable and non-draggable Polyline:
export const drawFreehand = (onDrawingComplete: Function) => {
// the freehand polyline
const polyline = new google.maps.Polyline({
map: window.map,
clickable: false,
draggable: false,
});
... Next, we'll listen to the mousemove event and update the polyline's path by calling .push() on it:
...
// the move-listener
const move = google.maps.event.addListener(
window.map,
'mousemove',
(e: google.maps.MapMouseEvent) => {
polyline.getPath().push(e.latLng);
}
);
... A polyline's path can also be updated by overriding its path like so:
polyline.setPath([...]) or polyline.setOptions({ path: [...] }) but polyline.getPath().push(...) is more elegant.
Finally, we'll register a one-time mouseup event on the map.
In its handler, we'll flush the move listener.
Notice how we stored it in a variable above to be able to remove it now.
The same applies to the mousedown event.
This time, we're using .clearListeners() which is a handy wrapper around the DOM's .removeEventListener() utility.
Next, we'll construct a Polygon from the polyline's path, turn the map's interactivity back on, and call the onDrawingComplete callback to swap the ”pencil/trash” buttons:
// the mouseup-listener
google.maps.event.addListenerOnce(window.map, 'mouseup', () => {
google.maps.event.removeListener(move);
google.maps.event.clearListeners(window.map.getDiv(), 'mousedown');
// get rid of the polyline used for drawing assistance
polyline.setMap(null);
// construct a polygon from the polyline's path
window.drawnPolygon = new google.maps.Polygon({
map: window.map,
paths: polyline.getPath().getArray(),
draggable: true,
editable: true,
});
toggleMapInteractivity(true);
// we're done
onDrawingComplete();
});
}; And that's all there's to it. The drawing functionality is self-closing and the final polygon is draggable & editable:
Now that you understand the mechanics of freehand drawing, here's how to implement the same functionality with Terra Draw.
If you're currently not using Google Maps' DrawingManager, you do NOT need to migrate to Terra Draw.
That said, Terra Draw handles the event management, touch support, and self-closing polygons for you:
import { TerraDraw, TerraDrawFreehandMode } from 'terra-draw';
import { TerraDrawGoogleMapsAdapter } from 'terra-draw-google-maps-adapter';
let terraDraw: TerraDraw;
const initTerraDraw = () => {
...
map.addListener("projection_changed", () => {
terraDraw = new TerraDraw({
adapter: new TerraDrawGoogleMapsAdapter({
map,
lib: google.maps,
coordinatePrecision: 6
}),
modes: [new TerraDrawFreehandMode()]
});
terraDraw.start();
// Wait for adapter to be ready
terraDraw.on('ready', () => {
bindControlBtns();
});
});
};
const bindControlBtns = () => {
const pencilBtn = document.getElementById("pencil") as HTMLButtonElement;
const trashBtn = document.getElementById("trash") as HTMLButtonElement;
pencilBtn.addEventListener("click", () => {
terraDraw.setMode('freehand');
toggleMapCls();
togglePencilVisibility();
});
trashBtn.addEventListener("click", () => {
terraDraw.clear();
toggleTrashVisibility();
togglePencilVisibility();
toggleMapCls();
});
terraDraw.on('finish', () => {
terraDraw.setMode('static');
toggleMapCls();
toggleTrashVisibility();
togglePencilVisibility();
triggerSearch();
});
}; Here's a migrated Terra Draw freehand drawing implementation:
Some geospatial databases don't support intersecting polygons so you may need to split the drawn polygon at the points of intersection.
To check if a polygon is self-intersecting, use the @turf/kinks utillity .
To split the polygon, use @turf/unkink-polygon .
No matter if you're using Google's DrawingManager or your own freehand drawing tool, you may want to check out Google's Snap to Roads API .
Now that you've put Drawing in your Google Maps toolkit, it's time to talk about 🔄 4. Interoperability & Conversions .