npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

roadzilla

v0.1.0

Published

Sophisticated road rendering for MapLibre map stylesheets

Readme

roadzilla

Roadzilla generates road layers for MapLibre stylesheets to achieve sophisticated road cartography.

an example map with road layers generated by Roadzilla

Overview

Roads are one of the hardest parts of a general-purpose map style to get right. Good maps render them with two different strategies depending on zoom. At high zoom, roads draw in physical order: overpasses cross over the roads beneath them, tunnels pass underneath. At low zoom, physical accuracy is less important than visual hierarchy, so major roads are drawn on top of minor roads to ensure that the most important links in the network appear uninterrupted.

Each strategy takes many MapLibre style layers to implement. Physical ordering needs a separate pass per physical layer, and within each pass every road's case (outline) must draw before any road's core (fill) so that at-grade intersections look connected. The hierarchical scheme needs its own set of layers with careful minzoom/maxzoom management, since a road class may climb the hierarchy as you zoom in (a primary road might start out grouped with minor roads, then get promoted) before handing off to physical rendering.

Roadzilla generates all of these layers from a compact config. It returns them in groups, one per physical layer, which you interleave with the rest of your style: tunnels below buildings, bridges above, and so on. By default, Roadzilla is compatible with vector tiles in the Sourdough tile schema for OpenStreetMap data, but by overriding some config values you can adapt it to work with most other schemas too. The colors, widths, zoom thresholds, and road class hierarchy assignments are all configurable.

Roadzilla is a library, not a complete style. It assumes you are already building your stylesheet with JavaScript or TypeScript rather than maintaining raw JSON. See the Sourdough examples repo for patterns on how to do this.

Installation

npm install roadzilla

Roadzilla has no runtime dependencies and comes with TypeScript types.

Usage

import roadzilla from "roadzilla";

const roads = roadzilla({ layers: [-1, 0, 1] });

const style = {
  version: 8,
  name: "My Custom Map Style",
  sources: { sourdough: { type: "vector", url: "https://..." } },
  layers: [
    // background, landuse, water...
    ...roads[0], // tunnels (layer <= -1), drawn under buildings
    // buildings, barriers, trails...
    ...roads[1], // surface roads (layer == 0 or null), plus the low-zoom hierarchy
    ...roads[2], // bridges (layer >= 1)
    // labels, POIs...
  ],
};

The return value is an array with one group of layers per entry in config.layers. Insert each group wherever it belongs in your layer stack.

How it works

Roadzilla's config is built around a few core ideas:

Kinds are the road classes you want to render: motorway, residential, service, and so on. Each feature's kind is determined by an expression, typically ["get", "highway"] or ["get", "class"] or similar. Each kind can have distinct colors, line widths, and other styling.

Families are the levels of the low-zoom hierarchy. Each kind's config says which families it belongs to and at what zoom it joins each one. For example, the default config has primary: { minor: 7, major: 9 }: primary roads first appear at z7 as minor roads, then get promoted to the major level at z9. Each family has a maxzoom at which it dissolves, releasing its member kinds into the physical order regime.

Layer buckets are the physically ordered groups used at high zoom. You choose which OSM layer values get their own bucket (e.g. layers: [-1, 0, 1]); values that are outside the range get clamped to the endpoints. This trades a little visual accuracy for a smaller and more performant stylesheet. Within a bucket, all cases draw before all cores, which makes at-grade intersections look visually connected.

bridge and tunnel tags play no part in physical ordering; that is layer's job. They are instead available to data-driven paint expressions, so you can dim tunnel cores or give bridges thicker casing (the defaults do both).

Roadzilla also renders cul-de-sacs, turning loops, and mini roundabouts as circle layers slotted into the surface (layer = 0) bucket. See turnings below.

Configuration

Every config property is optional. The example below shows all of them, set to their default values.

const roads = roadzilla({
  // Physical layer buckets: consecutive integers in ascending order. Roads
  // with layer values outside this range are clamped into the buckets at each
  // end. OSM elements can technically have any layer value, but in practice
  // nearly all roads fall between -2 and +2.
  layers: [-1, 0, 1],

  // Source and source-layer of the roads data in the vector tileset.
  source: "sourdough",
  sourceLayer: "highways",

  // Prefix for generated layer ids. Give each call a distinct prefix if you
  // use roadzilla more than once in the same style.
  idPrefix: "road",

  // Expressions for reading a road feature's kind and physical layer.
  getKind: ["get", "highway"],
  getLayer: ["coalesce", ["get", "layer"], 0],

  // Low-zoom hierarchy levels, in draw order (first is bottom-most). Each
  // family dissolves into physical rendering at its maxzoom. `cased: false`
  // renders a family as bare cores with no outline. Replaces the default
  // wholesale; spread `defaults.families` to extend it instead.
  families: [
    { name: "minor", maxzoom: 15, cased: false },
    { name: "major", maxzoom: 13 },
    { name: "arterial", maxzoom: 13 },
  ],

  // Road kinds to render, and which low-zoom families they belong to. A value
  // like { minor: 7, major: 9 } means that road kind first appears at z7 in
  // the minor family, and is promoted to the major family at z9.
  kinds: {
    service: { minor: 12 },
    busway: { minor: 12 },
    living_street: { minor: 11, major: 14 },
    pedestrian: { minor: 11, major: 14 },
    unclassified: { minor: 11, major: 14 },
    residential: { minor: 11, major: 14 },
    tertiary_link: { major: 11 },
    secondary_link: { major: 11 },
    primary_link: { major: 11 },
    trunk_link: { arterial: 11 },
    motorway_link: { arterial: 11 },
    tertiary: { minor: 10, major: 11 },
    secondary: { minor: 9, major: 10 },
    primary: { minor: 7, major: 9 },
    trunk: { arterial: 3 },
    motorway: { arterial: 3 },
  },

  // The color, width, and cap properties below all accept either a MapLibre
  // expression (which applies to highways of all kinds) or a function which
  // takes highway kind and returns such an expression (which will be applied
  // just to that kind). Object lookup tables are also supported as a shorthand
  // for the function variant (keys are highway kinds, values are expressions).

  // Core (fill) color. The default dims tunnels and tints motorways and trunks.
  coreColor: (highway) => {
    const base =
      highway.includes("motorway") || highway.includes("trunk")
        ? "hsl(220, 20%, 85%)"
        : "hsl(0, 0%, 100%)";
    const dim = "hsl(220, 15%, 92%)";
    return ["case", ["has", "tunnel"], dim, base];
  },

  // Case (outline) color. The default darkens bridges.
  caseColor: (highway) => {
    const base =
      highway.includes("motorway") || highway.includes("trunk")
        ? "hsl(220, 20%, 75%)"
        : "hsl(220, 20%, 85%)";
    const bridge = "hsl(220, 20%, 65%)";
    return ["case", ["has", "bridge"], bridge, base];
  },

  // By default, Roadzilla scales the widths of roads automatically as you zoom
  // in, with each road appearing as a hairline and increasing to its target
  // width at z18. The target width varies by the factor in the table below,
  // which can be customized by overriding it in the config.
  widthFactor: {
    motorway: 1.5,
    trunk: 1.4,
    primary: 1.3,
    secondary: 1.2,
    tertiary: 1.1,
    motorway_link: 0.7,
    trunk_link: 0.6,
    primary_link: 0.5,
    secondary_link: 0.4,
    tertiary_link: 0.4,
    service: 0.5,
    busway: 0.5,
    pedestrian: 0.875,
    default: 1.0,
  },

  // Alternately, you can define your own coreWidth function which returns
  // a MapLibre expression. widthFactor is ignored in this case and you can
  // control the width exactly as you see fit for each road class.
  coreWidth: (highway) => {
    return ["interpolate", ["linear"], ["zoom"], ...];
  }

  // Case width, in pixels per side of the road. The default thickens bridge
  // casings slightly.
  caseWidth: [
    "interpolate", ["linear"], ["zoom"],
    6, 0,
    18, ["case", ["has", "bridge"], 2.0, 1.5],
  ],

  // line-cap for cases and cores in physical rendering. (Low-zoom family
  // rendering always uses round caps currently)
  // 
  // Data-driven line-cap values, including the default caseCap below, require
  // MapLibre GL JS >= 5.22 and are not yet supported in MapLibre Native.
  caseCap: {
    motorway: "butt",
    trunk: "butt",
    primary: "butt",
    secondary: "butt",
    tertiary: "butt",
    motorway_link: "butt",
    trunk_link: "butt",
    primary_link: "butt",
    secondary_link: "butt",
    tertiary_link: "butt",
    default: ["case", ["has", "bridge"], "butt", "round"],
  },
  coreCap: "round",

  // Circle rendering for turning circles, mini roundabouts, etc. Set turnings:
  // false to disable.
  turnings: {
    // radius in meters (turning circles are rendered at constant physical size)
    radius: 8,
    // zoom where circles first appear; if omitted, defaults to the zoom
    // where the last family dissolves (15 with the default families)
    minzoom: 15,
    // kinds (returned by getKind) which should be rendered as circles
    kinds: ["turning_circle", "turning_loop", "mini_roundabout"],
    // subset of kinds which should get a center dot
    island: ["turning_loop", "mini_roundabout"],
    // dot radius (fraction of total radius)
    islandRatio: 0.35,
  },
});

The defaults export holds the full default config, so you can build on individual pieces of it, e.g. roadzilla({ kinds: { ...defaults.kinds, service: 14 } }).

Using other tile schemas

Roadzilla assumes a Sourdough-shaped schema by default, but should work with most other schemas if you override everything that reads feature properties: getKind, getLayer, kinds, widthFactor, the bridge/tunnel checks inside coreColor/caseColor/caseWidth, and turnings.kinds. For example, with OpenMapTiles you would set getKind: ["get", "class"] and declare kinds using OpenMapTiles class names.

Design notes

Roadzilla's default config draws inspiration from HighRoad, by Michal Migurski, Nathaniel Kelso and Geraldine Sarmiento of Stamen Design.

HighRoad helped popularize the treatment of roads that has become standard in digital maps today: physical order at high zoom, hierarchy order at low zoom, with three distinct classes in the hierarchy at every zoom level.

Roadzilla can be customized to use fewer or more families (hierarchy levels), but three is a good choice for most maps. The flexibility mainly exists so you can choose which kinds land in each level and when they appear, in order to achieve a more or less dense road visualization on your map.

License

This code is available under the ISC license; see the LICENSE file for details.