You open a style JSON. It's 2,000+ lines. You rename a source. You update the layers you remember. You deploy. Two days later someone asks why the rivers are gone. You check the map config — looks fine. You check the source URLs — fine. You grep the codebase, nothing obvious. Eventually you diff the style JSON against last week's version and find it: one layer still referencing the old source name. One line. Two days. The file is valid JSON. It passes schema checks. The map mostly works. But "mostly" is doing a lot of work there.
Mapbox Studio and Maputnik catch a lot while you're editing. But they're visual tools — they don't cover every custom source URL or every expression that degrades performance at scale. And more importantly: they're not in your CI pipeline.
Once a style file lands in a git repo, things drift. Someone edits it in VS Code. A build script modifies it programmatically. A PR renames a source but misses one of the three layers referencing it. In production, vector tiles load but features are invisible and no one knows why.
There's no ESLint for map styles. No cargo clippy. No automated gate between "looks fine locally" and "merged to main."
styl is that gate.
What styl does
styl is a CLI linter, validator, and formatter for MapLibre GL and Mapbox GL style JSON — built to run in CI, pre-commit hooks, and local development on raw files.
$ styl check style.json
error[E003] sources.roads: vector source missing required field "url" or "tiles"
--> style.json
warning[W002] layers[9].layout.visibility: layer "Building" is permanently invisible
--> style.json
hint: remove the layer or set visibility to "visible" if it should be shown
warning[W011] layers[3].filter: layer "Landuse" uses deprecated legacy filter syntax
--> style.json
hint: migrate to expression-based filters: https://maplibre.org/maplibre-style-spec/expressions/
Both errors and warnings exit 1 — use .stylrc to tune severity per rule if a warning shouldn't block your build.
Three categories:
-
Spec violations (E-codes) — missing required fields, broken source references,
source-layermismatch (e.g. style saysroadsbut the tile hasroad), layers pointing to non-existent sources - Best-practice warnings (W-codes) — permanently invisible layers (which still consume draw calls), duplicate IDs, legacy filter syntax, deeply nested expressions, performance anti-patterns
-
Formatting — canonical key ordering via
styl fmt, with--checkmode that exits1if anything would change
The formatter matters beyond aesthetics: unsorted keys create noisy PR diffs where real changes hide behind key-order churn. styl fmt makes diffs clean and reviewable.
Drop It Into CI
jobs:
style-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install styl
run: curl -fsSL https://raw.githubusercontent.com/navidnabavi/styl/main/install.sh | bash
- name: Validate map styles
run: styl check --format github style.json
For locked-down CI environments, use Homebrew instead: brew install navidnabavi/tap/styl.
More integrations are on the way!
The --format github flag emits GitHub Actions annotations — errors appear inline on the diff. Use --format json for GitLab CI or custom tooling.
For pre-commit:
styl fmt --check style.json && styl check style.json
Install
# macOS / Linux via Homebrew
brew tap navidnabavi/tap
brew install styl
# One-liner (no Rust required)
curl -fsSL https://raw.githubusercontent.com/navidnabavi/styl/main/install.sh | bash
# Pinned version with Rust
cargo install --git https://github.com/navidnabavi/styl --tag v0.0.3
Stdin is supported too — pipe-friendly for GIS pipelines:
cat style.json | styl check --stdin
Per-Project Config
Not every warning fits every project. Drop a .stylrc at your project root:
[rules]
W002 = "error" # invisible layers are a hard error for us
W003 = "off" # some layers intentionally toggled at runtime
W011 = "warn" # legacy filters: warn but don't block
[format]
indent = 4
styl walks up the directory tree to find it — monorepo-friendly. The file is spec-neutral: same config works whether you're targeting MapLibre or Mapbox.
Both Specs Supported
styl defaults to MapLibre GL Style Spec v8. Pass --spec mapbox for Mapbox GL.
Spec-specific divergence points — expression support differences, deprecated properties — are tracked in issues. Contributions filling those gaps are welcome.
What's Coming
- VS Code extension — inline diagnostics as you type, no CLI required
- Zed extension — same, for Zed users
-
Watch mode —
styl watch style.jsonfor live feedback during local development
Adding a new lint rule is ~50 lines implementing a single trait — see src/linter/rules/visibility.rs as an example. If you have a pattern worth catching, open an issue or send a PR.
The next time a source rename breaks production tiles, you'll wish this was already in your pipeline.
If it saves you time, a ⭐ on the repo goes a long way.

Top comments (0)