North American road sign set: highway guide, exit, next services, mile marker, speed limit and warning diamond on green, blue and yellow faces. Uses the Overpass sign font and Phosphor service icons.
// Editorial road signs in the North American guide / service / regulatory /
// warning idiom: guide sign, exit, services, marker, speed limit and warning.
import {
BedIcon,
ForkKnifeIcon,
GasPumpIcon,
} from "@phosphor-icons/react/dist/ssr"
import { cn } from "cn"
const FACE_GREEN = "bg-[oklch(0.47_0.092_171.22)]"
const FACE_BLUE = "bg-[oklch(0.323_0.103_253.89)]"
const FACE_YELLOW = "bg-[oklch(0.861_0.173_91.94)]"
const TEXT_GREEN = "text-[oklch(0.47_0.092_171.22)]"
// Secondary legend is a face-cast white, not a dimmed one — it recedes by hue.
// L=0.95 is the floor that clears 4.5:1 through the face wash; measured.
const LEGEND_GREEN = "text-[oklch(0.95_0.03_171)]"
const LEGEND_BLUE = "text-[oklch(0.88_0.035_254)]"
// Mounted sign under directional light: tight and hard, never a floating card.
const SIGN_SHADOW = "shadow-[0_8px_18px_oklch(0_0_0/0.22)]"
// The legend is the content, so a sign takes no children.
type SignProps = Omit<React.ComponentProps<"div">, "children">
type SignService = "gas" | "food" | "lodging"
type SignDestination = {
name: string
/** Shown on the right of the name, such as "47". */
distance?: string
}
const SERVICE_GLYPHS = {
gas: { Icon: GasPumpIcon, label: "Gas" },
food: { Icon: ForkKnifeIcon, label: "Food" },
lodging: { Icon: BedIcon, label: "Lodging" },
} as const
function ServiceRow({ services }: { services: SignService[] }) {
if (services.length === 0) return null
return (
<div className="mt-2 flex gap-2">
{services.map((service, index) => {
const { Icon, label } = SERVICE_GLYPHS[service]
return (
<span
key={`${service}-${index}`}
className="grid size-6 place-items-center rounded-xs bg-[oklch(1_0_0)]"
>
<Icon
weight="fill"
className="size-4 text-[oklch(0_0_0)]"
aria-hidden
/>
<span className="sr-only">{label}</span>
</span>
)
})}
</div>
)
}
/**
* A green highway guide sign: an optional direction and route above a list
* of destinations, each with an optional distance on the right. An exit
* number, when given, sits on a tab over the top edge.
*/
function HighwaySign({
className,
route,
destinations = [],
exitNumber,
direction,
...props
}: SignProps & {
/** The route name, shown in a white box, such as "I-80". */
route?: string
/** The places listed on the sign, top to bottom. */
destinations?: SignDestination[]
/** Adds an "EXIT" tab with this number on the top edge, near the right. */
exitNumber?: string
/** Shown in capitals above the route. */
direction?: "north" | "south" | "east" | "west"
}) {
return (
<div
data-slot="highway-sign"
className={cn(
"relative rounded border-[3px] border-[oklch(1_0_0)] p-4 font-sign font-bold text-[oklch(1_0_0)]",
// A restrained face highlight so the sign is not digitally flat.
"before:pointer-events-none before:absolute before:inset-0 before:rounded before:bg-linear-to-b before:from-[oklch(1_0_0)]/10 before:to-transparent before:content-['']",
FACE_GREEN,
SIGN_SHADOW,
className
)}
{...props}
>
{exitNumber && (
<div
className={cn(
"absolute -top-3 right-4 rounded-xs border-2 border-[oklch(1_0_0)] px-2 py-0.5 text-xs/5",
FACE_GREEN
)}
>
EXIT {exitNumber}
</div>
)}
{direction && (
<div className={cn("mb-1 text-xs/5 tracking-wider", LEGEND_GREEN)}>
{direction.toUpperCase()}
</div>
)}
{route && (
<div className="mb-2 flex">
<div
className={cn(
"rounded-xs bg-[oklch(1_0_0)] px-2 py-0.5 text-sm/6 font-extrabold",
TEXT_GREEN
)}
>
{route}
</div>
</div>
)}
<div className="flex flex-col gap-1">
{destinations.map((destination, index) => (
<div
key={`${index}-${destination.name}`}
className="flex items-center justify-between gap-8 text-lg/7"
>
<span>{destination.name}</span>
{destination.distance && (
<span className={cn("whitespace-nowrap", LEGEND_GREEN)}>
{destination.distance}
</span>
)}
</div>
))}
</div>
</div>
)
}
/**
* A green exit sign: a yellow "EXIT" tab on the top-right corner, the
* destination, an optional distance, and an optional row of service icons.
*/
function ExitSign({
className,
exitNumber,
destination,
distance,
services = [],
...props
}: SignProps & {
/** The number on the yellow "EXIT" tab. */
exitNumber: string
/** The place the exit leads to, the sign's main line of text. */
destination: string
/** Shown under the destination, such as "1/2 MILE". */
distance?: string
/**
* Icons in a row under the text: "gas", "food" or "lodging". Each has a
* label for screen readers.
*/
services?: SignService[]
}) {
return (
<div
data-slot="exit-sign"
className={cn(
"relative rounded-[3px] border-2 border-[oklch(1_0_0)] p-3 font-sign font-bold text-[oklch(1_0_0)]",
FACE_GREEN,
className
)}
{...props}
>
<div
className={cn(
"absolute -top-2 -right-2 rounded-xs px-2 py-0.5 text-xs/5 font-extrabold text-[oklch(0_0_0)]",
FACE_YELLOW
)}
>
EXIT {exitNumber}
</div>
<div className="text-lg/7">{destination}</div>
{distance && (
<div className={cn("mt-1 text-sm/6", LEGEND_GREEN)}>{distance}</div>
)}
<ServiceRow services={services} />
</div>
)
}
/**
* A blue services sign that reads "NEXT SERVICES", a large mile count and
* "MILES", with an optional row of service icons at the bottom.
*/
function NextExitSign({
className,
miles,
services = [],
...props
}: SignProps & {
/** The distance to the next services, shown large above "MILES". */
miles: number
/**
* Icons in a row at the bottom: "gas", "food" or "lodging". Each has a
* label for screen readers.
*/
services?: SignService[]
}) {
return (
<div
data-slot="next-exit-sign"
className={cn(
"rounded-[3px] border-2 border-[oklch(1_0_0)] p-4 text-center font-sign font-bold text-[oklch(1_0_0)]",
FACE_BLUE,
className
)}
{...props}
>
<div className={cn("text-sm/6 tracking-wider", LEGEND_BLUE)}>NEXT</div>
<div className={cn("mb-2 text-xs/5 tracking-wider", LEGEND_BLUE)}>
SERVICES
</div>
<div className="text-4xl/12 font-extrabold">{miles}</div>
<div className="text-sm/6 tracking-wider">MILES</div>
<div className="flex justify-center">
<ServiceRow services={services} />
</div>
</div>
)
}
/**
* A small green mile marker: an optional route name, the mile number and the
* word "MILE".
*/
function MileMarker({
className,
mile,
route,
...props
}: SignProps & {
/** The mile number, shown large above the word "MILE". */
mile: number
/** Shown small above the mile number, such as "I-80". */
route?: string
}) {
return (
<div
data-slot="mile-marker"
className={cn(
"min-w-16 rounded-xs border border-[oklch(1_0_0)]/50 px-2 py-3 text-center font-sign font-bold text-[oklch(1_0_0)]",
FACE_GREEN,
className
)}
{...props}
>
{route && (
<div className={cn("text-xs/4 tracking-wide", LEGEND_GREEN)}>
{route}
</div>
)}
<div className="text-xl/7">{mile}</div>
<div className={cn("text-xs/4 tracking-wider", LEGEND_GREEN)}>MILE</div>
</div>
)
}
/** A white regulatory sign that reads "SPEED LIMIT" over a large number. */
function SpeedLimitSign({
className,
speed,
...props
}: SignProps & {
/** The limit, shown large under "SPEED LIMIT". */
speed: number
}) {
return (
<div
data-slot="speed-limit-sign"
className={cn(
"w-fit min-w-20 rounded-[3px] border-2 border-[oklch(0_0_0)] bg-[oklch(1_0_0)] px-3 py-4 text-center font-sign text-[oklch(0_0_0)]",
className
)}
{...props}
>
<div className="text-xs/4 font-bold tracking-wider">SPEED</div>
<div className="text-xs/4 font-bold tracking-wider">LIMIT</div>
<div className="mt-2 text-5xl/12 font-extrabold">{speed}</div>
</div>
)
}
/**
* A yellow diamond warning sign with a short message, and an optional second
* line under it.
*
* The element's box is the diamond's full footprint, so no corner hangs
* outside it, and the text sits in the square inside the diamond, so it stays
* on the yellow. Resize it with a `size-*` class: the diamond and the text
* area scale with it, the type does not.
*/
function WarningSign({
className,
text,
subtext,
...props
}: SignProps & {
/** The main message, such as "CURVE". */
text: string
/** A smaller second line under the message, such as "AHEAD". */
subtext?: string
}) {
return (
<div
data-slot="warning-sign"
className={cn("relative size-[11.3136rem] font-sign", className)}
{...props}
>
<div
className={cn(
"absolute inset-[calc((100%-100%/1.4142)/2)] rotate-45 rounded-[3px] border-2 border-[oklch(0_0_0)]",
FACE_YELLOW
)}
/>
<div className="absolute inset-0 m-auto flex size-1/2 flex-col items-center justify-center font-extrabold text-[oklch(0_0_0)]">
<span className="px-2 text-center text-sm/5">{text}</span>
{subtext && <span className="mt-1 text-xs/4">{subtext}</span>}
</div>
</div>
)
}
export {
ExitSign,
HighwaySign,
MileMarker,
NextExitSign,
SpeedLimitSign,
WarningSign,
type SignDestination,
type SignService,
}
pnpm dlx shadcn add @01orbital/road-signsnpx shadcn add @01orbital/road-signsyarn shadcn add @01orbital/road-signsbunx --bun shadcn add @01orbital/road-signsFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/graphics/road-signs.tsx, as source you own and edit.
import {
ExitSign,
HighwaySign,
MileMarker,
NextExitSign,
SpeedLimitSign,
WarningSign,
} from "@/components/graphics/road-signs"
// Every word on the page. Change copy here, not in the markup.
const COPY = {
route: "I-80",
exit: "Riverside",
distance: "1/2 MILE",
warning: "CURVE",
warning2: "AHEAD",
} as const
const DESTINATIONS = [
{ name: "Cheyenne", distance: "47" },
{ name: "Salt Lake City", distance: "432" },
] as const
// Night ground: the faces are cut for a dark page — on white they flatten
// into stickers.
export default function RoadSignsDemo() {
return (
<>
<div className="flex max-w-4xl flex-wrap items-end justify-center gap-8">
<HighwaySign
direction="west"
route={COPY.route}
destinations={[...DESTINATIONS]}
/>
<NextExitSign miles={47} services={["gas", "food"]} />
<ExitSign
exitNumber="82"
destination={COPY.exit}
distance={COPY.distance}
services={["gas", "food", "lodging"]}
/>
<SpeedLimitSign speed={65} />
<WarningSign text={COPY.warning} subtext={COPY.warning2} />
<MileMarker mile={214} route={COPY.route} />
</div>
</>
)
}Road Signs is built from 6 parts. In the example they nest like this:
HighwaySign
NextExitSign
ExitSign
SpeedLimitSign
WarningSign
MileMarkerHighwaySign: A green highway guide sign: an optional direction and route above a list of destinations, each with an optional distance on the right. An exit number, when given, sits on a tab over the top edge.NextExitSign: A blue services sign that reads "NEXT SERVICES", a large mile count and "MILES", with an optional row of service icons at the bottom.ExitSign: A green exit sign: a yellow "EXIT" tab on the top-right corner, the destination, an optional distance, and an optional row of service icons.SpeedLimitSign: A white regulatory sign that reads "SPEED LIMIT" over a large number.WarningSign: A yellow diamond warning sign with a short message, and an optional second line under it.MileMarker: A small green mile marker: an optional route name, the mile number and the word "MILE".A green highway guide sign: an optional direction and route above a list of destinations, each with an optional distance on the right. An exit number, when given, sits on a tab over the top edge.
Prop
Type
It also accepts the HTML attributes of the element it renders.
A blue services sign that reads "NEXT SERVICES", a large mile count and "MILES", with an optional row of service icons at the bottom.
Prop
Type
It also accepts the HTML attributes of the element it renders.
A green exit sign: a yellow "EXIT" tab on the top-right corner, the destination, an optional distance, and an optional row of service icons.
Prop
Type
It also accepts the HTML attributes of the element it renders.
A white regulatory sign that reads "SPEED LIMIT" over a large number.
Prop
Type
It also accepts the HTML attributes of the element it renders.
A yellow diamond warning sign with a short message, and an optional second line under it.
The element's box is the diamond's full footprint, so no corner hangs outside it, and the text sits in the square inside the diamond, so it stays on the yellow. Resize it with a size-* class: the diamond and the text area scale with it, the type does not.
Prop
Type
It also accepts the HTML attributes of the element it renders.
A small green mile marker: an optional route name, the mile number and the word "MILE".
Prop
Type
It also accepts the HTML attributes of the element it renders.
npm: @phosphor-icons/react, cn. The CLI installs them for you.
Registry pieces it installs with it:
font-overpassOrbit System
Orbital diagram with two dashed rings, a chambray planet and two smaller moons in cocoa and celadon, painted with Orbital v4 orbs. Absolutely centred and scaled down on small screens.
Scratch Reveal
Teal painted panel whose paint wears away on hover to expose a pencil construction sketch, with the label switching to canvas ink. Set in Marcellus; decorative, and reduced motion skips the fade.