← Back to Crumbs
DashboardsReactTypeScriptTailwind
Frontier Rail Control
A segmented control drawn as a frontier rail line - each option's name is lettered on a wooden signpost board along a two-rail track, and a little train, engine and three carriages, travels the track to whichever station you pick, tumbleweed drifting along the bottom. The option text never moves; only the train does.
Live preview
Code
frontier-rail-control.tsx
"use client";
import * as React from "react";
export interface FrontierRailOption {
value: string;
label: string;
}
export interface FrontierRailControlProps {
options: FrontierRailOption[];
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
className?: string;
}
const STATION_WIDTH = 64;
const GAP = 20;
const STEP = STATION_WIDTH + GAP;
const LOCOMOTIVE_WIDTH = 24;
const CARRIAGE_WIDTH = 17;
const CARRIAGE_COUNT = 3;
const COUPLER_GAP = 2;
const MARKER_WIDTH =
CARRIAGE_WIDTH * CARRIAGE_COUNT + COUPLER_GAP * CARRIAGE_COUNT + LOCOMOTIVE_WIDTH;
const TRACK_HEIGHT = 4;
function Carriage() {
return (
<span aria-hidden="true" className="relative block h-[13px] w-[17px]">
<span className="absolute inset-x-0 top-[2px] h-[7px] rounded-[2px] bg-[#8b5a3c] dark:bg-[#a8734d]" />
<span className="absolute left-[6px] top-[3px] h-[3px] w-[3px] rounded-[1px] bg-[#fbf4ec] dark:bg-[#241c16]" />
<span className="absolute bottom-0 left-[1px] h-[4px] w-[4px] rounded-full bg-[#3a2c1f] dark:bg-[#a8927a]" />
<span className="absolute bottom-0 left-[11px] h-[4px] w-[4px] rounded-full bg-[#3a2c1f] dark:bg-[#a8927a]" />
</span>
);
}
function Locomotive() {
return (
<span aria-hidden="true" className="relative block h-[15px] w-6">
<span className="absolute left-[4px] top-0 h-[5px] w-[2.5px] rounded-t-[1px] bg-[#3a2c1f] dark:bg-[#a8927a]" />
<span
className="absolute inset-x-0 top-[4px] h-[7px] bg-[#c1502e] shadow-[0_2px_4px_rgba(193,80,46,0.45)] dark:bg-[#e8875f]"
style={{ borderRadius: "1px 999px 999px 1px / 1px 6px 6px 1px" }}
/>
<span className="absolute left-[10px] top-[5.5px] h-[4px] w-[4px] rounded-[1px] bg-[#fbf4ec] dark:bg-[#241c16]" />
<span className="absolute bottom-0 left-[2px] h-[4px] w-[4px] rounded-full bg-[#3a2c1f] dark:bg-[#a8927a]" />
<span className="absolute bottom-0 left-[15px] h-[4px] w-[4px] rounded-full bg-[#3a2c1f] dark:bg-[#a8927a]" />
</span>
);
}
function Train() {
return (
<span className="flex items-end" style={{ gap: COUPLER_GAP }}>
<Carriage />
<Carriage />
<Carriage />
<Locomotive />
</span>
);
}
function RailTrack() {
return (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 bottom-0"
style={{ height: TRACK_HEIGHT }}
>
<div className="absolute inset-x-0 top-0 h-[1.5px] bg-[#8b6f4e]/50 dark:bg-[#a8927a]/35" />
<div className="absolute inset-x-0 bottom-0 h-[1.5px] bg-[#8b6f4e]/50 dark:bg-[#a8927a]/35" />
<div
className="absolute inset-0"
style={{
backgroundImage:
"repeating-linear-gradient(90deg, rgba(139,111,78,0.45) 0 2px, transparent 2px 11px)",
}}
/>
</div>
);
}
function Tumbleweed() {
return (
<>
<style href="frontier-rail-tumbleweed-keyframes" precedence="default">{`
@keyframes frontier-rail-tumbleweed-roll {
0% { left: -20px; transform: rotate(0deg); opacity: 0; }
8% { opacity: 0.7; }
92% { opacity: 0.7; }
100% { left: 100%; transform: rotate(1080deg); opacity: 0; }
}
`}</style>
<span
aria-hidden="true"
className="pointer-events-none absolute bottom-1 h-3 w-3 rounded-full"
style={{
background:
"repeating-conic-gradient(from 0deg, #9c8556 0deg 9deg, transparent 9deg 24deg)",
animation: "frontier-rail-tumbleweed-roll 12s linear infinite",
}}
/>
</>
);
}
export function FrontierRailControl({
options,
value,
defaultValue,
onChange,
className = "",
}: FrontierRailControlProps) {
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = React.useState(defaultValue ?? options[0]?.value);
const selected = isControlled ? value : internalValue;
const selectedIndex = Math.max(
options.findIndex((o) => o.value === selected),
0,
);
const stationRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
function select(index: number) {
const option = options[index];
if (!option) return;
if (!isControlled) setInternalValue(option.value);
onChange?.(option.value);
}
function handleKeyDown(e: React.KeyboardEvent, index: number) {
let next = index;
if (e.key === "ArrowRight") next = Math.min(index + 1, options.length - 1);
else if (e.key === "ArrowLeft") next = Math.max(index - 1, 0);
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = options.length - 1;
else return;
e.preventDefault();
select(next);
stationRefs.current[next]?.focus();
}
return (
<div
className={[
"relative inline-block overflow-hidden rounded-2xl bg-[#f7ecd8] px-6 pb-6 pt-5 shadow-[0_2px_10px_rgba(90,60,30,0.08)] dark:bg-[#241c16]",
className,
]
.filter(Boolean)
.join(" ")}
>
<div role="radiogroup" className="relative flex items-end" style={{ gap: GAP }}>
<RailTrack />
<div
aria-hidden="true"
className="pointer-events-none absolute bottom-0 transition-transform duration-500 ease-out"
style={{
left: STATION_WIDTH / 2 - MARKER_WIDTH / 2,
transform: `translateX(${selectedIndex * STEP}px)`,
}}
>
<Train />
</div>
{options.map((option, i) => {
const isSelected = i === selectedIndex;
return (
<button
key={option.value}
type="button"
role="radio"
aria-checked={isSelected}
tabIndex={isSelected ? 0 : -1}
ref={(el) => {
stationRefs.current[i] = el;
}}
onClick={() => select(i)}
onKeyDown={(e) => handleKeyDown(e, i)}
style={{ width: STATION_WIDTH }}
className="group relative z-10 flex shrink-0 flex-col items-center transition-transform duration-200 ease-out hover:-translate-y-1 hover:scale-110"
>
<span
className={
isSelected
? "rounded-[3px] border-2 border-[#8b5a3c] bg-[#c1502e] px-2.5 py-1 text-[12px] font-bold text-white shadow-[0_2px_4px_rgba(0,0,0,0.18)] dark:border-[#5c3a26] dark:bg-[#e8875f] dark:text-[#241c16]"
: "rounded-[3px] border-2 border-[#8b6f4e]/50 bg-[#e8d4b0] px-2 py-[3px] text-[11px] font-semibold text-[#5c4530] transition-colors duration-200 ease-out group-hover:border-[#c1502e]/50 group-hover:bg-[#f0e0bd] dark:border-[#6b5a45]/60 dark:bg-[#3a2e22] dark:text-[#c9b596] dark:group-hover:bg-[#453728]"
}
>
{option.label}
</span>
<span
className={
isSelected
? "h-5 w-[3px] bg-[#8b5a3c] dark:bg-[#5c3a26]"
: "h-2 w-[2.5px] bg-[#a8927a] transition-colors duration-200 ease-out group-hover:bg-[#8b6f4e] dark:bg-[#6b5a45]"
}
/>
</button>
);
})}
</div>
<Tumbleweed />
</div>
);
}
Usage
usage.tsx
import { FrontierRailControl } from "@/components/frontier-rail-control";
export function DateRangeSwitch() {
return (
<FrontierRailControl
options={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
]}
defaultValue="week"
onChange={(value) => console.log(value)}
/>
);
}Dependencies
None - just React and Tailwind.
Responsive
Each station is a fixed 64px wide so the train's travel distance can be computed with plain arithmetic instead of measuring the DOM - the whole line's width scales with however many options you pass in, so keep labels short enough to fit a small board. Works as an uncontrolled component (defaultValue) or controlled (value + onChange). Keyboard-navigable with arrow keys, Home, and End, like a native radio group.
Dark mode
The sandy backdrop switches to a warm dark brown via dark:, with the route line, station boards, and signposts adjusting to stay legible against it. The train and the selected station's terracotta accent brighten slightly in dark mode so they still read clearly; the tumbleweed stays the same dusty tone in both.