Once UI Handbook
Vibe coding
Introduction to vibe coding
Set up your local dev environment
Essential tools for vibe coding
Build your first Once UI app
Getting started
What is Once UI?
Install & config
Page skeleton
Your first page
Upgrade from 1.8 to 2.0
Once UI without Next.js
Build and launch
How to launch a portfolio site that actually gets you hired
How to build a documentation site with MDX and Next.js
How to ship a landing page and dashboard with authentication in Next.js
How to launch a social app with Next.js and Supabase
Common patterns
Hero section
Static panel
Form layout
Highlighted card
Responsive stacking
Decoration layers
Dialog & modal
Toast & feedback
Tables & lists
Loading states
App shell navigation
Dashboard & charts
Chat & messaging
Auth & verification
Settings & split panels
Data filters & toolbar
Command palette
Pricing & plans
Accordion & FAQ
Media & uploads
Empty & error states
SEO, Open Graph, and structured data
Documentation code blocks with live preview
Carousels, galleries, and before/after comparisons
Date pickers and scheduling
Context menus and dropdown actions
Tags, chips, and multi-value inputs
Roadmap & kanban
Form inputs & controls
Progress, status & badges
Scrolling & feeds
Profile, avatars & identity
Social proof & logo clouds
Masonry & media grids
Table pagination, search & bulk selection
Banners & announcements
Footer layouts
Onboarding & first run
Waitlist & coming soon
Forms that wire themselves
An icon rail that opens on hover
Maps without an API key
Design tips
Color & surfaces
Harness overview
Row, Column & Grid
Spacing & rhythm
Icons & buttons
Reveal & motion
Theme tokens, FOUC-free init, and runtime style controls
Tooltips and hover cards
Border radius & nested corners
Bring your own icons
Give one section its own style
Override Once UI styles without !important
Agent resources
Column, not Flex
Semantics over CSS
Typography scale
Common gotchas
Block anchors
Validate generated code
Pro block registry
Match task bundles
Block updates
Block updates changelog
TrademarkTrademark
Ctrl k
Search…
Sign up
Once UI Handbook
Vibe coding
Introduction to vibe coding
Set up your local dev environment
Essential tools for vibe coding
Build your first Once UI app
Getting started
What is Once UI?
Install & config
Page skeleton
Your first page
Upgrade from 1.8 to 2.0
Once UI without Next.js
Build and launch
How to launch a portfolio site that actually gets you hired
How to build a documentation site with MDX and Next.js
How to ship a landing page and dashboard with authentication in Next.js
How to launch a social app with Next.js and Supabase
Common patterns
Hero section
Static panel
Form layout
Highlighted card
Responsive stacking
Decoration layers
Dialog & modal
Toast & feedback
Tables & lists
Loading states
App shell navigation
Dashboard & charts
Chat & messaging
Auth & verification
Settings & split panels
Data filters & toolbar
Command palette
Pricing & plans
Accordion & FAQ
Media & uploads
Empty & error states
SEO, Open Graph, and structured data
Documentation code blocks with live preview
Carousels, galleries, and before/after comparisons
Date pickers and scheduling
Context menus and dropdown actions
Tags, chips, and multi-value inputs
Roadmap & kanban
Form inputs & controls
Progress, status & badges
Scrolling & feeds
Profile, avatars & identity
Social proof & logo clouds
Masonry & media grids
Table pagination, search & bulk selection
Banners & announcements
Footer layouts
Onboarding & first run
Waitlist & coming soon
Forms that wire themselves
An icon rail that opens on hover
Maps without an API key
Design tips
Color & surfaces
Harness overview
Row, Column & Grid
Spacing & rhythm
Icons & buttons
Reveal & motion
Theme tokens, FOUC-free init, and runtime style controls
Tooltips and hover cards
Border radius & nested corners
Bring your own icons
Give one section its own style
Override Once UI styles without !important
Agent resources
Column, not Flex
Semantics over CSS
Typography scale
Common gotchas
Block anchors
Validate generated code
Pro block registry
Match task bundles
Block updates
Block updates changelog
TrademarkTrademark
Once UIDocumentationBlog
© Once UI. All rights reserved.
Built with Aveiro
Common Patterns

Maps without an API key

How to put an interactive, theme-aware map on a page with Once UI's Map module: markers from data, clustering, popups, areas, and the props that keep it friendly on long pages.
Updated 2 hours ago
An icon rail that opens on hover
4 min · Pattern · Map
From @once-ui-system/core 2.6.0, a map with markers, pins and popups is one import, with free tiles and no API key.

Why a map used to be a project

Putting a map on a page usually meant signing up for a tile provider, pasting a key into the client, and then fighting a second design system: markers, popups and controls that looked nothing like the rest of the app. For a listings page, a venue finder or a contact page, that is a lot of work for one rectangle.

What core ships now

@once-ui-system/core/map draws an interactive map on MapLibre GL with OpenFreeMap tiles. The data is OpenStreetMap, there is no key and no view limit. The basemap follows your light or dark theme and is tinted with the page background token, so it sits on the same surface as the page. maplibre-gl is an optional peer, so add it next to core. It only loads when a map actually renders.
npm i @once-ui-system/core maplibre-gl

The smallest map

A map is a Column, so it takes the usual layout props. Give it a height, a camera, and something to mark.
import { Map, MapMarker, type LngLat } from "@once-ui-system/core/map";

const BUDAPEST: LngLat = [19.0402, 47.4979];

export function OfficeMap() {
  return (
    <Map height={24} defaultViewport={{ center: BUDAPEST, zoom: 12 }} labels={{ map: "Our office" }}>
      <MapMarker position={BUDAPEST} />
    </Map>
  );
}
Positions are [longitude, latitude], the order MapLibre and GeoJSON use. Copying coordinates from a search result often gives you latitude first, so check that first if your pin lands in the ocean.

Markers are React

MapMarker takes any content: a MapPin, an Avatar, a button. It stays real DOM inside your theme, so a link in a marker is reachable by Tab, and focusing one off screen pans it into view. MapPin is the marker Once UI draws for you: a pill with a label such as a price, or a dot when it has no children. With href it is a link, with onClick a button.

Many places from data

For a list of results, hand the data to MapMarkers and let bounds frame it.
"use client";

import { useState } from "react";
import { Map, MapMarkers, MapPin, MapPopup, type LngLat } from "@once-ui-system/core/map";
import { Text } from "@once-ui-system/core";

type Venue = { id: string; name: string; position: LngLat };

export function VenueMap({ venues }: { venues: Venue[] }) {
  const [open, setOpen] = useState<Venue>();

  return (
    <Map height={32} bounds={venues.map((venue) => venue.position)} labels={{ map: "Venues" }}>
      <MapMarkers
        items={venues}
        cluster
        activeId={open?.id}
        renderItem={(venue) => (
          <MapPin active={venue.id === open?.id} onClick={() => setOpen(venue)}>
            {venue.name}
          </MapPin>
        )}
      />
      {open && (
        <MapPopup position={open.position} onClose={() => setOpen(undefined)}>
          <Text variant="label-strong-m">{open.name}</Text>
        </MapPopup>
      )}
    </Map>
  );
}
A few things this does without extra code:
  • bounds refits when the list changes, so a new set of search results is always in view. boundsMaxZoom (15 by default) stops a single result from filling the screen.
  • cluster merges overlapping markers into a count pill that zooms in when pressed. It is worked out per zoom level, so clusters hold still while you pan.
  • activeId lifts one marker above the rest, which is what you want when a row in a list beside the map is hovered.
  • MapPopup is a surface card you render while it is open, like a Dialog. Escape and the close button call onClose.

An area instead of a point

When you shouldn't show an exact address, or you want a delivery radius, draw a MapCircle. The radius is in metres and the colour is a token family, brand by default.

Small props worth knowing

Prop
What it does
`cooperativeGestures`
Scroll zooms only with Ctrl or ⌘, touch pans with two fingers
`interactive={false}`A static map with no pan or zoom
`controls={false}`Hides the zoom buttons
`mapStyle`
Your own basemap, or a `{ light, dark }` pair
Turn on cooperativeGestures for any map in the middle of a long page, so people scrolling past don't get stuck zooming. If you switch to a keyed tile provider, pass the key through transformRequest rather than baking it into a style URL.
Keyboard panning and zoom, reduced motion and a fallback message without WebGL are built in. Set labels.map to a real name, since it is the map's accessible name.

When you add click handlers

Map mounts on the client only, because WebGL has nothing to draw on the server. The smallest map above needs nothing extra, but as soon as you pass onClick, state or onViewportChange, put the map in a file that starts with "use client", as in the venue example.
More layout patterns live in this section.
<MapCircle position={[19.0545, 47.5065]} radius={400} color="accent" />