# Features

_Last updated October 19, 2026_

> Each feature is a GeoJSON Feature with a string id. Its properties hold its details, what visitors get from it, and how it's drawn.

> **Beta:** squidmaps is in beta. Features, the map spec and these docs may change without notice, and things may occasionally break. Export a copy of any map that matters to you, and tell us when something isn't right: use Beta feedback, or email support@squidmaps.io.

## Geometry

Points, lines and polygons, their Multi- forms, and geometry collections, as
in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1).
Coordinates are `[longitude, latitude]`, with at most six decimal places. A
Point can also be [text](#feature-text), and a Polygon of four corners an
[image](#feature-image). A Polygon can also be a
[circle](#feature-circle-area) or a [rectangle](#feature-rectangle-area),
made from its center and radius, or its center, size and rotation.

## Order

A map's `features` are listed top first, as in the editor's list. Points
and text are drawn above lines, areas and images, whatever their place.
Among points and text, and among lines, areas and images, features earlier
in the list are drawn on top. One exception keeps outlines readable: between
two images, areas' fills are drawn under the lines and outlines listed with
them.

## Properties

Everything about a feature is in its GeoJSON `properties`: its own data,
like `name`, and how it looks and behaves, under keys starting with `@`, so
styling travels with the feature.

Every property is optional: a feature without one gets its default, so a
plain GeoJSON point is drawn as the default circle. A value that doesn't
match its property makes the feature, and its map, invalid. Properties
squidmaps doesn't use are kept as they are.

## Common properties

What a feature is, and what visitors get from it on the published map.

### Details

**All features.** What the feature is. Other properties without an "@" are the feature's own data, kept as they are.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | None | Shown in its tooltip, popup and label, and suggested for its legend row. Text's name is its words, drawn on the map. |
| `description` | `string` | None | Markdown, shown in its popup. HTML is escaped, and links open in a new tab. |
| `@folder` | `string` (1 to 100 characters) | None | The folder it's in, by name. Folders are one level deep, and exist while a feature names them. |
| `@images` | `array of FeatureImage` (at most 24 items) | None | Its images, cover first: at most 24, uploaded in the editor. |
| `@locked` | `boolean` | `false` | Locked in the editor: it can be selected, but not moved, reshaped, restyled or deleted until it's unlocked. Visitors' maps ignore it. |

### Tooltip

**Points, lines and areas.** A small box following the pointer while visitors hover over the feature.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@tooltip` | `boolean` | `false` | Whether hovering over the feature shows a tooltip. |
| `@tooltip-name` | `boolean` | `true` | Show the feature's name. |
| `@tooltip-thumbnail` | `boolean` | `false` | Show a thumbnail of its first image, if it has one. |
| `@tooltip-background` | `Color` | `"#000000"` | The tooltip's color. |
| `@tooltip-color` | `Color` | `"#FFFFFF"` | The text's color. |

```json
{
  "type": "Feature",
  "id": "area-1",
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [-73.9564, 40.773816],
        [-73.9546, 40.773816],
        [-73.9546, 40.775184],
        [-73.9564, 40.775184],
        [-73.9564, 40.773816]
      ]
    ]
  },
  "properties": {
    "name": "Carl Schurz Park",
    "@tooltip": true,
    "@tooltip-background": "#2F8A4E",
    "@line-color": "#2F8A4E",
    "@fill-color": "#2F8A4E"
  }
}
```

### Popup

**Points, lines and areas.** What visitors get when they click the feature.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@popup` | `"card" \| "panel" \| "none"` | `"card"` | What clicking the feature opens: a card above it with its images, name and description; the same in a panel beside the map; or nothing. |
| `@directions` | `boolean` | `false` | For points: offer directions to it, in Google Maps, from its popup. |

```json
{
  "type": "Feature",
  "id": "cafe",
  "geometry": {
    "type": "Point",
    "coordinates": [-73.9555, 40.7745]
  },
  "properties": {
    "name": "Café on the corner",
    "description": "Get the **cardamom bun**, and a seat by the window.",
    "@popup": "card",
    "@fill": "#D9664E",
    "@radius": 11,
    "@icon": "solid/mug-hot"
  }
}
```

## Points

Points are drawn as 3 types (circle, emoji and marker), chosen by `@point-type`. Each type has
its own properties, so a point keeps every type's style and changing its
type back restores it.

### Point type

**Points.** How a point is drawn. Each type keeps its own style properties, so changing a point's type and back restores its style.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@point-type` | `"circle" \| "emoji" \| "marker"` | `"circle"` | Which of the point styles below it's drawn with. |

```json
[
  {
    "type": "Feature",
    "id": "point-1",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9579, 40.7745]
    },
    "properties": {
      "name": "A circle"
    }
  },
  {
    "type": "Feature",
    "id": "point-2",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9555, 40.7745]
    },
    "properties": {
      "name": "A marker pin",
      "@point-type": "marker"
    }
  },
  {
    "type": "Feature",
    "id": "point-3",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9531, 40.7745]
    },
    "properties": {
      "name": "An emoji",
      "@point-type": "emoji",
      "@emoji": "🌳"
    }
  }
]
```

### Circles

**Points.** A colored circle, optionally with an icon in it. For points with "@point-type": "circle".

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@fill` | `Color` | `"#3F80C4"` | The circle's color. |
| `@radius` | `number` (2 to 40) | `8` | In pixels, not counting the stroke. |
| `@stroke` | `Color` | `"#FFFFFF"` | The ring around the circle. |
| `@stroke-width` | `number` (0 to 10) | `2` | The ring's width in pixels, outside the radius. |
| `@icon` | `Icon` | None | A Font Awesome icon in the circle. |
| `@icon-color` | `Color` | None | The icon's color. Without one, white or dark ink, whichever contrasts the fill. |

```json
[
  {
    "type": "Feature",
    "id": "circle-1",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9591, 40.7745]
    },
    "properties": {}
  },
  {
    "type": "Feature",
    "id": "circle-2",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9567, 40.7745]
    },
    "properties": {
      "@fill": "#D9664E",
      "@radius": 13,
      "@icon": "solid/mug-hot"
    }
  },
  {
    "type": "Feature",
    "id": "circle-3",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9543, 40.7745]
    },
    "properties": {
      "@fill": "#12312C",
      "@radius": 5,
      "@stroke": "#F2C14E",
      "@stroke-width": 4
    }
  },
  {
    "type": "Feature",
    "id": "circle-4",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9519, 40.7745]
    },
    "properties": {
      "@fill": "#F2C14E",
      "@radius": 18,
      "@stroke-width": 0,
      "@icon": "solid/star",
      "@icon-color": "#8A3B12"
    }
  }
]
```

### Emoji

**Points.** An emoji, drawn as its Twemoji image. For points with "@point-type": "emoji".

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@emoji` | `Emoji` | `"📍"` | One emoji, drawn as its Twemoji image. |
| `@size` | `number` (12 to 64) | `28` | The emoji's width and height in pixels. |

```json
[
  {
    "type": "Feature",
    "id": "emoji-1",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9579, 40.7745]
    },
    "properties": {
      "@point-type": "emoji",
      "@emoji": "🍕",
      "@size": 20
    }
  },
  {
    "type": "Feature",
    "id": "emoji-2",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9555, 40.7745]
    },
    "properties": {
      "@point-type": "emoji",
      "@emoji": "🚲",
      "@size": 32
    }
  },
  {
    "type": "Feature",
    "id": "emoji-3",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9531, 40.7745]
    },
    "properties": {
      "@point-type": "emoji",
      "@emoji": "🏛️",
      "@size": 48
    }
  }
]
```

### Marker pins

**Points.** A map pin standing on the point, with an optional icon in its head. For points with "@point-type": "marker".

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@marker-shell` | `Color` | `"#FFFFFF"` | The pin's outer color. |
| `@marker-disc` | `Color` | `"#0E9384"` | The disc in the pin's head. |
| `@marker-icon` | `Icon` | None | A Font Awesome icon on the disc. Without one, it's empty. |
| `@marker-icon-color` | `Color` | None | The icon's color. Without one, white or dark ink, whichever contrasts the disc. |

```json
[
  {
    "type": "Feature",
    "id": "marker-1",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9579, 40.7745]
    },
    "properties": {
      "@point-type": "marker"
    }
  },
  {
    "type": "Feature",
    "id": "marker-2",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9555, 40.7745]
    },
    "properties": {
      "@point-type": "marker",
      "@marker-shell": "#C2473A",
      "@marker-disc": "#FFFFFF",
      "@marker-icon": "solid/utensils"
    }
  },
  {
    "type": "Feature",
    "id": "marker-3",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9531, 40.7745]
    },
    "properties": {
      "@point-type": "marker",
      "@marker-shell": "#12312C",
      "@marker-disc": "#F2C14E",
      "@marker-icon": "solid/star",
      "@marker-icon-color": "#12312C"
    }
  }
]
```

### Labels

**Points.** A point's name, drawn beside it. Labels belong to the point, not its type.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@show-label` | `boolean` | `false` | Whether the point's name is drawn beside it. Labels that would overlap are hidden until there's room, unless they ignore conflicts. |
| `@label-size` | `number` (10 to 24) | `13` | The text's size in pixels. |
| `@label-color` | `Color` | `"#12312C"` | The text's color. Its halo is white or dark ink, to contrast. |
| `@label-placement` | `"top" \| "right"` | `"top"` | Centered above the point, or to its right. |
| `@label-ignore-conflicts` | `boolean` | `false` | Whether the label always shows, even where it overlaps other labels or points. |

```json
[
  {
    "type": "Feature",
    "id": "label-1",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9579, 40.7745]
    },
    "properties": {
      "name": "Above",
      "@show-label": true
    }
  },
  {
    "type": "Feature",
    "id": "label-2",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9555, 40.7745]
    },
    "properties": {
      "name": "To the right",
      "@show-label": true,
      "@label-placement": "right"
    }
  },
  {
    "type": "Feature",
    "id": "label-3",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9531, 40.7745]
    },
    "properties": {
      "name": "Bigger, in color",
      "@show-label": true,
      "@label-size": 17,
      "@label-color": "#C2473A",
      "@fill": "#C2473A"
    }
  }
]
```

## Lines

**Lines.** How a line is drawn. When a property is missing, its simplestyle-spec equivalent is read instead, so GeoJSON styled for other tools keeps its look.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@line-color` | `Color` | `"#3F80C4"` | The line's color. Falls back to `stroke`. |
| `@line-width` | `number` (0.5 to 20) | `3` | In pixels. Falls back to `stroke-width`. |
| `@line-opacity` | `number` (0 to 1) | `1` | From 0 (invisible) to 1 (opaque). Falls back to `stroke-opacity`. |
| `@line-dasharray` | `array of number` (at most 8 items, each 0 to 100) | `[]` | Dash and gap lengths in turn, in line widths, like [4, 2]. Empty is solid. |
| `@line-cap` | `"butt" \| "round" \| "square"` | `"round"` | How the line's ends are drawn. |

```json
[
  {
    "type": "Feature",
    "id": "line-1",
    "geometry": {
      "type": "LineString",
      "coordinates": [
        [-73.96, 40.77565],
        [-73.957, 40.77605],
        [-73.954, 40.77565],
        [-73.951, 40.77605]
      ]
    },
    "properties": {
      "@line-color": "#3F80C4",
      "@line-width": 6
    }
  },
  {
    "type": "Feature",
    "id": "line-2",
    "geometry": {
      "type": "LineString",
      "coordinates": [
        [-73.96, 40.77475],
        [-73.957, 40.77515],
        [-73.954, 40.77475],
        [-73.951, 40.77515]
      ]
    },
    "properties": {
      "@line-color": "#C2473A",
      "@line-width": 3,
      "@line-dasharray": [4, 2],
      "@line-cap": "butt"
    }
  },
  {
    "type": "Feature",
    "id": "line-3",
    "geometry": {
      "type": "LineString",
      "coordinates": [
        [-73.96, 40.77385],
        [-73.957, 40.77425],
        [-73.954, 40.77385],
        [-73.951, 40.77425]
      ]
    },
    "properties": {
      "@line-color": "#12312C",
      "@line-width": 4,
      "@line-dasharray": [0, 2]
    }
  },
  {
    "type": "Feature",
    "id": "line-4",
    "geometry": {
      "type": "LineString",
      "coordinates": [
        [-73.96, 40.77295],
        [-73.957, 40.77335],
        [-73.954, 40.77295],
        [-73.951, 40.77335]
      ]
    },
    "properties": {
      "@line-color": "#2F8A4E",
      "@line-width": 10,
      "@line-opacity": 0.5
    }
  }
]
```

## Areas

**Areas.** How an area is drawn: its outline, like a line's, and its fill. Missing properties fall back to simplestyle-spec too.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@line-color` | `Color` | `"#3F80C4"` | The line's color. Falls back to `stroke`. |
| `@line-width` | `number` (0.5 to 20) | `2` | In pixels. Falls back to `stroke-width`. |
| `@line-opacity` | `number` (0 to 1) | `1` | From 0 (invisible) to 1 (opaque). Falls back to `stroke-opacity`. |
| `@line-dasharray` | `array of number` (at most 8 items, each 0 to 100) | `[]` | Dash and gap lengths in turn, in line widths, like [4, 2]. Empty is solid. |
| `@line-cap` | `"butt" \| "round" \| "square"` | `"round"` | How the line's ends are drawn. |
| `@fill-color` | `Color` | `"#3F80C4"` | The area's inside color. Falls back to `fill`. |
| `@fill-opacity` | `number` (0 to 1) | `0.3` | From 0 (no fill) to 1 (opaque). Falls back to `fill-opacity`. |

```json
[
  {
    "type": "Feature",
    "id": "area-1",
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [
          [-73.9592, 40.773816],
          [-73.9574, 40.773816],
          [-73.9574, 40.775184],
          [-73.9592, 40.775184],
          [-73.9592, 40.773816]
        ]
      ]
    },
    "properties": {}
  },
  {
    "type": "Feature",
    "id": "area-2",
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [
          [-73.9564, 40.773816],
          [-73.9546, 40.773816],
          [-73.9546, 40.775184],
          [-73.9564, 40.775184],
          [-73.9564, 40.773816]
        ]
      ]
    },
    "properties": {
      "@line-color": "#2F8A4E",
      "@line-width": 1,
      "@fill-color": "#2F8A4E",
      "@fill-opacity": 0.6
    }
  },
  {
    "type": "Feature",
    "id": "area-3",
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [
          [-73.9536, 40.773816],
          [-73.9518, 40.773816],
          [-73.9518, 40.775184],
          [-73.9536, 40.775184],
          [-73.9536, 40.773816]
        ]
      ]
    },
    "properties": {
      "@line-color": "#C2473A",
      "@line-width": 3,
      "@line-dasharray": [2, 2],
      "@fill-opacity": 0
    }
  }
]
```

## Text

**Text.** Words on the map, centered on a point: a Point with "@feature-type": "text", whose name is drawn with the properties below. New lines in the name start new lines. Text isn't a kind of point, so it has no point type, label, tooltip or popup.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@feature-type` | `"text"` | None | Makes a Point text, drawn with the properties below. |
| `@text-size` | `number` (8 to 120) | `24` | The text's size in pixels; with "@text-scale": "map", its size at "@text-scale-zoom". |
| `@text-color` | `Color` | `"#12312C"` | The text's color. |
| `@text-halo-color` | `Color` | `"#FFFFFF"` | The outline around the letters, to keep them readable. |
| `@text-halo-width` | `number` (0 to 8) | `2` | The outline's width in pixels. 0 has none. |
| `@text-background` | `Color` | None | A rounded rectangle behind the text, in this color. Without one, there's no background. |
| `@text-background-radius` | `number` (0 to 32) | `6` | The background's corner radius in pixels. |
| `@text-scale` | `"pixels" \| "map"` | `"pixels"` | "pixels" stays the same size at every zoom; "map" grows and shrinks with the map. |
| `@text-scale-zoom` | `number` (0 to 24) | `12` | With "@text-scale": "map", the zoom at which the text is "@text-size" pixels: twice that one zoom in, half one out. |
| `@text-rotate` | `number` (-180 to 180) | `0` | How far the text is turned about its point, in degrees clockwise, from -180 to 180. 0 is level. |

```json
[
  {
    "type": "Feature",
    "id": "text-1",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9589, 40.7745]
    },
    "properties": {
      "@feature-type": "text",
      "name": "Plain"
    }
  },
  {
    "type": "Feature",
    "id": "text-2",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9555, 40.7745]
    },
    "properties": {
      "@feature-type": "text",
      "name": "With a\nbackground",
      "@text-size": 16,
      "@text-color": "#FFFFFF",
      "@text-halo-width": 0,
      "@text-background": "#1E7A6C"
    }
  },
  {
    "type": "Feature",
    "id": "text-3",
    "geometry": {
      "type": "Point",
      "coordinates": [-73.9521, 40.7745]
    },
    "properties": {
      "@feature-type": "text",
      "name": "Turned",
      "@text-size": 20,
      "@text-color": "#C2473A",
      "@text-rotate": -20,
      "@text-scale": "map",
      "@text-scale-zoom": 16
    }
  }
]
```

## Circle areas

**Areas.** An area that stays a circle: a Polygon with "@feature-type": "circle" and its center and radius. The polygon is made from them, round on the map, so send null (or any Polygon) as its geometry and the API fills it in. It's styled as an area. Without the two properties below it's a plain area, whose vertices can be edited.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@feature-type` | `"circle"` | None | Makes an area a circle, made from the property below. |
| `@circle` | `object` | None | A circle's center and radius. It's drawn round on the map (Web Mercator), so its radius is exact at its center. |
| `@circle.center` | `[number, number]` (items in turn: -180 to 180; -85.051129 to 85.051129) | None | Its center, as [longitude, latitude]. |
| `@circle.radius` | `number` (0.1 to 5000000) | None | In meters, at its center. |

```json
{
  "type": "Feature",
  "id": "circle-area",
  "geometry": null,
  "properties": {
    "name": "A 150 m walk",
    "@feature-type": "circle",
    "@circle": {
      "center": [-73.9555, 40.7745],
      "radius": 150
    },
    "@fill-color": "#F2C14E"
  }
}
```

## Rectangles

**Areas.** An area that stays a rectangle, at any angle: a Polygon with "@feature-type": "rectangle" and its center, size and rotation. The polygon is made from them, a rectangle on the map, so send null (or any Polygon) as its geometry and the API fills it in. It's styled as an area. Without the two properties below it's a plain area, whose vertices can be edited.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@feature-type` | `"rectangle"` | None | Makes an area a rectangle, made from the property below. |
| `@rectangle` | `object` | None | A rectangle's center, size and turn. It's drawn as a rectangle on the map (Web Mercator), so its size is exact at its center. |
| `@rectangle.center` | `[number, number]` (items in turn: -180 to 180; -85.051129 to 85.051129) | None | Its center, as [longitude, latitude]. |
| `@rectangle.width` | `number` (0.1 to 10000000) | None | In meters, at its center: its top and bottom edges. |
| `@rectangle.height` | `number` (0.1 to 10000000) | None | In meters, at its center: its left and right edges. |
| `@rectangle.rotation` | `number` (-180 to 180) | None | How far it's turned clockwise, in degrees: 0 is upright, with its top edge running east–west. |

```json
{
  "type": "Feature",
  "id": "rectangle",
  "geometry": null,
  "properties": {
    "name": "A lot",
    "@feature-type": "rectangle",
    "@rectangle": {
      "center": [-73.9555, 40.7745],
      "width": 320,
      "height": 140,
      "rotation": 29
    }
  }
}
```

The API makes a circle's or rectangle's polygon when it saves the map, so
send its geometry as `null` (or any Polygon). The editor resizes them by
their parameters, so they stay circles and rectangles; removing
`@feature-type` and the parameter makes either a plain area, whose vertices
can be edited.

## Images

**Images.** A picture fixed to the map, drawn among lines and areas in the features' order: a Polygon of its four corners (top left, top right, bottom right, bottom left, then the first again) with "@feature-type": "image". The corners place, size and turn the picture.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `@feature-type` | `"image"` | None | Makes a Polygon an image, placed by its corners. |
| `@image` | `FeatureImage` | None | The picture, uploaded in the editor. |

The editor moves an image's corners together, keeping the picture in
proportion. Images are drawn under every point and text, and aren't
interactive on published maps.

## Types

Objects inside feature properties.

### FeatureImage

An image uploaded to squidmaps, kept as uploaded and scaled down.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` (a UUID) | Yes |  |
| `url` | `string` (a URL) | Yes | The folder its scaled-down WebP versions are in, each at {url}/{size}.webp: thumb (256px), small (640px), medium (1280px), large (2560px). |
| `original` | `string` (a URL) | Yes | The file as uploaded. |
| `width` | `integer` (above 0) | Yes | The original's width in pixels, upright. |
| `height` | `integer` (above 0) | Yes | The original's height in pixels, upright. |
| `bytes` | `integer` (at least 0) | Yes | Bytes in storage: the original and its scaled versions together. |
| `alt` | `string` (at most 1000 characters) | No | Alt text: what the image shows, for people who can't see it, read out by screen readers. Missing for an image without one. |

## Value types

Kinds of values that appear in several places.

### Bounds

An area as [west, south, east, north], in degrees. West is below east and south below north; it doesn't cross the antimeridian. Coordinates have at most six decimal places.

`[number, number, number, number]`: items in turn: -180 to 180; -85.051129 to 85.051129; -180 to 180; -85.051129 to 85.051129

### Color

A color in hex, as "#rrggbb", like "#3F80C4".

`string`: matches /^#[0-9a-fA-F]{6}$/

### Icon

A Font Awesome 6 Free icon as "style/name": "solid/mug-hot", "regular/heart" or "brands/github". The style is solid, regular or brands.

`string`: matches /^(solid|regular|brands)\/[a-z0-9]+(-[a-z0-9]+)*$/

### Emoji

One emoji, like "📍", "🇯🇵" or "👩‍🚒", including flags and sequences joined with zero-width joiners.

`string`
