Building a Moon Phase Display

javascript
// Get moon data for New York today
const response = await fetch("https://worlddataapi.com/v1/moon/40.71,-74.01", {
	headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await response.json();

console.log(data.phase_name); // "Waning Gibbous"
console.log(data.illumination); // 72.5
console.log(data.moonrise); // "2026-01-15T21:45:00-05:00"
python
import requests

response = requests.get(
    "https://worlddataapi.com/v1/moon/40.71,-74.01",
    headers={"X-API-Key": "YOUR_API_KEY"}
)
data = response.json()

print(data["phase_name"])   # "Waning Gibbous"
print(data["illumination"]) # 72.5
print(data["moonrise"])     # "2026-01-15T21:45:00-05:00"
bash
curl -X GET "https://worlddataapi.com/v1/moon/40.71,-74.01" \
  -H "X-API-Key: YOUR_API_KEY"

This guide covers building a moon phase display for your application, from fetching data to creating visual representations of lunar phases.

The Challenge#

Displaying accurate moon phases requires understanding the lunar cycle's complexity. The moon progresses through eight principal phases over approximately 29.5 days, but the illumination percentage changes continuously. Different applications need different levels of precision: a simple calendar might only show the four major phases, while a photography app needs exact illumination percentages and moonrise times.

Location matters too. Moonrise and moonset times vary by latitude and longitude, and sometimes the moon doesn't rise or set at all on a given day. Your display logic needs to handle these edge cases gracefully.

Prerequisites#

Before you start, you need:

  • A World Data API key (get one at worlddataapi.com)

  • Basic knowledge of JavaScript or Python

  • Understanding of async/await patterns for API calls

  • A location (latitude and longitude) for your queries

API Response Structure#

The moon endpoint returns:

json
{
	"location": { "latitude": 40.71, "longitude": -74.01 },
	"date": "2026-01-15",
	"timezone": "America/New_York",
	"moonrise": "2026-01-15T21:45:00-05:00",
	"moonset": "2026-01-16T10:32:00-05:00",
	"phase": "waning_gibbous",
	"phase_name": "Waning Gibbous",
	"illumination": 72.5,
	"age_days": 18.3
}

Phase Values#

phasephase_nameIllumination Range
newNew Moon0%
waxing_crescentWaxing Crescent1-49% (increasing)
first_quarterFirst Quarter50% (right half lit)
waxing_gibbousWaxing Gibbous51-99% (increasing)
fullFull Moon100%
waning_gibbousWaning Gibbous99-51% (decreasing)
last_quarterLast Quarter50% (left half lit)
waning_crescentWaning Crescent49-1% (decreasing)

Basic Implementation#

javascript
async function getMoonPhase(latitude, longitude, date = null) {
	const url = date
		? `https://worlddataapi.com/v1/moon/${latitude},${longitude}?date=${date}`
		: `https://worlddataapi.com/v1/moon/${latitude},${longitude}`;

	const response = await fetch(url, {
		headers: { "X-API-Key": API_KEY },
	});

	if (!response.ok) {
		throw new Error(`API error: ${response.status}`);
	}

	return response.json();
}

// Get today's moon phase
const moon = await getMoonPhase(40.71, -74.01);
console.log(`${moon.phase_name} - ${moon.illumination}% illuminated`);

Visual Representation#

Using Emoji#

The quickest approach — map phases to moon emoji:

javascript
const MOON_EMOJI = {
	new: "\u{1F311}", // new moon
	waxing_crescent: "\u{1F312}",
	first_quarter: "\u{1F313}",
	waxing_gibbous: "\u{1F314}",
	full: "\u{1F315}", // full moon
	waning_gibbous: "\u{1F316}",
	last_quarter: "\u{1F317}",
	waning_crescent: "\u{1F318}",
};

function getMoonEmoji(phase) {
	return MOON_EMOJI[phase] || "\u{1F319}";
}

// Usage
const moon = await getMoonPhase(40.71, -74.01);
console.log(`${getMoonEmoji(moon.phase)} ${moon.phase_name}`);
// Output: [moon emoji] Waning Gibbous

CSS Moon#

Create a moon using CSS gradients:

jsx
function CSSMoon({ phase, illumination, size = 100 }) {
	const getGradient = () => {
		// Simplified: actual implementation needs hemisphere calculation
		if (phase === "new") {
			return "radial-gradient(circle, #1a1a2e 100%, #1a1a2e 100%)";
		}
		if (phase === "full") {
			return "radial-gradient(circle, #f5f5dc 100%, #f5f5dc 100%)";
		}

		const isWaxing = phase.includes("waxing") || phase === "first_quarter";
		const litPercent = illumination;

		if (isWaxing) {
			// Right side lit
			return `linear-gradient(90deg, #1a1a2e ${100 - litPercent}%, #f5f5dc ${100 - litPercent}%)`;
		} else {
			// Left side lit
			return `linear-gradient(90deg, #f5f5dc ${litPercent}%, #1a1a2e ${litPercent}%)`;
		}
	};

	return (
		<div
			className="css-moon"
			style={{
				width: size,
				height: size,
				borderRadius: "50%",
				background: getGradient(),
				boxShadow: "inset 0 0 20px rgba(0,0,0,0.3)",
			}}
		/>
	);
}

SVG Moon with Accurate Illumination#

More accurate visual using SVG clipping:

jsx
function SVGMoon({ illumination, phase, size = 100 }) {
	const isWaxing =
		phase.includes("waxing") ||
		phase === "first_quarter" ||
		phase === "full";

	// Calculate the curve of the terminator (shadow edge)
	const terminatorX = (illumination / 100) * size;

	// For accurate rendering, we need to account for the spherical shape
	const curveOffset = Math.sin((illumination / 100) * Math.PI) * (size * 0.2);

	return (
		<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
			{/* Dark side of the moon */}
			<circle
				cx={size / 2}
				cy={size / 2}
				r={size / 2 - 2}
				fill="#1a1a2e"
			/>

			{/* Lit portion - simplified as ellipse */}
			<clipPath id="moonClip">
				<circle cx={size / 2} cy={size / 2} r={size / 2 - 2} />
			</clipPath>

			<ellipse
				cx={
					isWaxing
						? size - (size * illumination) / 100 / 2
						: (size * illumination) / 100 / 2
				}
				cy={size / 2}
				rx={(size * illumination) / 100 / 2}
				ry={size / 2 - 2}
				fill="#f5f5dc"
				clipPath="url(#moonClip)"
			/>

			{/* Subtle crater texture */}
			<circle
				cx={size * 0.3}
				cy={size * 0.4}
				r={size * 0.08}
				fill="rgba(0,0,0,0.1)"
			/>
			<circle
				cx={size * 0.6}
				cy={size * 0.6}
				r={size * 0.12}
				fill="rgba(0,0,0,0.08)"
			/>
			<circle
				cx={size * 0.45}
				cy={size * 0.75}
				r={size * 0.06}
				fill="rgba(0,0,0,0.1)"
			/>
		</svg>
	);
}

Using Moon Phase Images#

For the most accurate visuals, use pre-rendered images:

jsx
function ImageMoon({ phase, size = 100 }) {
	// Assumes you have 8 moon phase images
	const imagePath = `/images/moon-phases/${phase}.png`;

	return (
		<img
			src={imagePath}
			alt={phase.replace("_", " ")}
			width={size}
			height={size}
			className="moon-image"
		/>
	);
}

Building a Complete Moon Widget#

jsx
function MoonWidget({ latitude, longitude }) {
	const [moonData, setMoonData] = useState(null);
	const [loading, setLoading] = useState(true);

	useEffect(() => {
		getMoonPhase(latitude, longitude)
			.then(setMoonData)
			.finally(() => setLoading(false));
	}, [latitude, longitude]);

	if (loading) return <div className="moon-widget loading">Loading...</div>;
	if (!moonData) return null;

	return (
		<div className="moon-widget">
			<SVGMoon
				illumination={moonData.illumination}
				phase={moonData.phase}
				size={120}
			/>

			<div className="moon-info">
				<h3>{moonData.phase_name}</h3>
				<p className="illumination">
					{Math.round(moonData.illumination)}% illuminated
				</p>
				<p className="age">
					Day {Math.round(moonData.age_days)} of lunar cycle
				</p>
			</div>

			<div className="moon-times">
				{moonData.moonrise && (
					<p>
						<span className="label">Moonrise:</span>
						<time>{formatTime(moonData.moonrise)}</time>
					</p>
				)}
				{moonData.moonset && (
					<p>
						<span className="label">Moonset:</span>
						<time>{formatTime(moonData.moonset)}</time>
					</p>
				)}
			</div>
		</div>
	);
}

function formatTime(isoString) {
	return new Date(isoString).toLocaleTimeString("en-US", {
		hour: "numeric",
		minute: "2-digit",
	});
}

Moon Calendar#

Display a month of moon phases:

jsx
function MoonCalendar({ latitude, longitude, year, month }) {
	const [phases, setPhases] = useState([]);
	const [loading, setLoading] = useState(true);

	useEffect(() => {
		const fetchMonth = async () => {
			const daysInMonth = new Date(year, month + 1, 0).getDate();
			const promises = [];

			for (let day = 1; day <= daysInMonth; day++) {
				const date = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
				promises.push(getMoonPhase(latitude, longitude, date));
			}

			const results = await Promise.all(promises);
			setPhases(results);
			setLoading(false);
		};

		fetchMonth();
	}, [latitude, longitude, year, month]);

	if (loading) return <div>Loading moon calendar...</div>;

	return (
		<div className="moon-calendar">
			{phases.map((day, i) => (
				<div key={i} className="calendar-day">
					<span className="day-number">{i + 1}</span>
					<span className="moon-emoji">
						{getMoonEmoji(day.phase)}
					</span>
					<span className="illumination">
						{Math.round(day.illumination)}%
					</span>
				</div>
			))}
		</div>
	);
}

Finding Key Lunar Events#

Next Full Moon#

javascript
async function findNextFullMoon(latitude, longitude, startDate = new Date()) {
	let currentDate = new Date(startDate);

	// The lunar cycle is ~29.5 days, so check the next 30 days
	for (let i = 0; i < 30; i++) {
		const dateStr = currentDate.toISOString().split("T")[0];
		const moon = await getMoonPhase(latitude, longitude, dateStr);

		if (moon.phase === "full") {
			return moon;
		}

		currentDate.setDate(currentDate.getDate() + 1);
	}

	return null;
}

All Key Phases in a Month#

javascript
async function getKeyPhasesInMonth(latitude, longitude, year, month) {
	const daysInMonth = new Date(year, month + 1, 0).getDate();
	const keyPhases = [];
	let previousPhase = null;

	for (let day = 1; day <= daysInMonth; day++) {
		const date = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
		const moon = await getMoonPhase(latitude, longitude, date);

		// Detect phase transitions
		if (moon.phase !== previousPhase) {
			if (
				["new", "first_quarter", "full", "last_quarter"].includes(
					moon.phase,
				)
			) {
				keyPhases.push({
					date: moon.date,
					phase: moon.phase,
					phase_name: moon.phase_name,
				});
			}
		}

		previousPhase = moon.phase;
	}

	return keyPhases;
}

// Usage
const phases = await getKeyPhasesInMonth(40.71, -74.01, 2026, 0);
// [
//   { date: '2026-01-03', phase: 'first_quarter', phase_name: 'First Quarter' },
//   { date: '2026-01-10', phase: 'full', phase_name: 'Full Moon' },
//   { date: '2026-01-17', phase: 'last_quarter', phase_name: 'Last Quarter' },
//   { date: '2026-01-25', phase: 'new', phase_name: 'New Moon' }
// ]

Handling Missing Moonrise/Moonset#

The moon doesn't always rise and set every day — sometimes it rises just before midnight and sets after the next midnight:

javascript
function describeMoonVisibility(moonData) {
	const { moonrise, moonset } = moonData;

	if (!moonrise && !moonset) {
		return {
			status: "always_visible",
			description: "The moon is above the horizon all day",
		};
	}

	if (!moonrise) {
		return {
			status: "no_rise",
			description: `Moon sets at ${formatTime(moonset)}, does not rise today`,
			note: "Moonrise occurs after midnight",
		};
	}

	if (!moonset) {
		return {
			status: "no_set",
			description: `Moon rises at ${formatTime(moonrise)}, does not set today`,
			note: "Moonset occurs after midnight",
		};
	}

	return {
		status: "normal",
		description: `Rises ${formatTime(moonrise)}, sets ${formatTime(moonset)}`,
	};
}

Use Cases#

Fishing/Hunting Apps#

Solunar theory suggests fish and game are more active during certain lunar periods:

javascript
function getSolunarRating(moonData) {
	const { phase, illumination } = moonData;

	// Major periods: around moonrise and moonset
	// Minor periods: moon overhead and underfoot

	// Full and new moons are considered best
	if (phase === "full" || phase === "new") {
		return { rating: 5, description: "Excellent — major solunar period" };
	}

	// Quarter moons are moderate
	if (phase === "first_quarter" || phase === "last_quarter") {
		return { rating: 3, description: "Average activity expected" };
	}

	// In-between phases
	if (illumination > 75 || illumination < 25) {
		return { rating: 4, description: "Good activity expected" };
	}

	return { rating: 2, description: "Below average activity" };
}

Gardening Apps#

Some gardeners plant according to moon phases:

javascript
function getGardeningAdvice(moonData) {
	const { phase } = moonData;

	const advice = {
		new: {
			activity: "Rest period",
			crops: "Not ideal for planting",
			tasks: "Good for weeding, pest control, and soil preparation",
		},
		waxing_crescent: {
			activity: "Increasing moonlight",
			crops: "Plant leafy greens, cabbage, spinach",
			tasks: "Good for planting above-ground crops",
		},
		first_quarter: {
			activity: "Strong growth period",
			crops: "Plant tomatoes, peppers, squash",
			tasks: "Continue planting above-ground crops",
		},
		waxing_gibbous: {
			activity: "Approaching full moon",
			crops: "Plant fruit-bearing plants",
			tasks: "Good for transplanting",
		},
		full: {
			activity: "Peak energy",
			crops: "Plant root vegetables",
			tasks: "Harvest, prune, fertilize",
		},
		waning_gibbous: {
			activity: "Decreasing moonlight",
			crops: "Plant root crops, bulbs",
			tasks: "Good for planting below-ground crops",
		},
		last_quarter: {
			activity: "Rest approaching",
			crops: "Continue root vegetables",
			tasks: "Harvest, cultivate, weed",
		},
		waning_crescent: {
			activity: "Dormant period approaching",
			crops: "Avoid planting",
			tasks: "Clear beds, prepare for next cycle",
		},
	};

	return advice[phase] || advice.new;
}

Astronomy Apps#

Combine with sun data for complete night sky planning:

javascript
async function getNightSkyConditions(latitude, longitude, date) {
	const [sun, moon] = await Promise.all([
		getSunData(latitude, longitude, date),
		getMoonPhase(latitude, longitude, date),
	]);

	const conditions = {
		astronomicalDarkness: {
			start: sun.twilight.astronomical.dusk,
			end: sun.twilight.astronomical.dawn,
		},
		moonInterference: getMoonInterference(moon),
		bestViewing: null,
	};

	// Calculate best deep-sky viewing window
	if (moon.illumination < 25) {
		conditions.bestViewing = {
			start: sun.twilight.astronomical.dusk,
			end: sun.twilight.astronomical.dawn,
			quality: "Excellent — minimal moon interference",
		};
	} else if (
		moon.moonset &&
		new Date(moon.moonset) < new Date(sun.twilight.astronomical.dusk)
	) {
		conditions.bestViewing = {
			start: sun.twilight.astronomical.dusk,
			end: sun.twilight.astronomical.dawn,
			quality: "Good — moon sets before darkness",
		};
	} else {
		conditions.bestViewing = {
			start: moon.moonset || sun.twilight.astronomical.dusk,
			end: moon.moonrise || sun.twilight.astronomical.dawn,
			quality: "Limited — plan around moon visibility",
		};
	}

	return conditions;
}

function getMoonInterference(moonData) {
	const { illumination, phase } = moonData;

	if (illumination > 80) {
		return {
			level: "high",
			description: "Bright moon — deep sky objects hard to see",
		};
	}
	if (illumination > 50) {
		return {
			level: "moderate",
			description: "Some interference — focus on bright objects",
		};
	}
	if (illumination > 25) {
		return {
			level: "low",
			description: "Minor interference — good viewing conditions",
		};
	}
	return {
		level: "minimal",
		description: "Dark skies — excellent for deep sky",
	};
}

Caching#

Moon data for a specific location and date is deterministic:

javascript
const cache = new Map();

async function getCachedMoonPhase(latitude, longitude, date) {
	const key = `${latitude.toFixed(2)},${longitude.toFixed(2)}:${date}`;

	if (!cache.has(key)) {
		const data = await getMoonPhase(latitude, longitude, date);
		cache.set(key, data);
	}

	return cache.get(key);
}

Common Pitfalls#

Assuming moonrise and moonset always occur. The moon's orbit means it sometimes doesn't rise or set on a given calendar day. Always check for null values before displaying times.

Ignoring hemisphere differences. The visual appearance of moon phases is inverted in the Southern Hemisphere. A waxing crescent appears on the right in the Northern Hemisphere but on the left in the Southern Hemisphere. Your SVG or CSS rendering should account for user location.

Not caching deterministic data. Moon phase data for a specific location and date doesn't change. Cache aggressively to reduce API calls, especially when building calendar views that fetch many dates at once.

Confusing illumination with phase. A 50% illumination can be either First Quarter (waxing) or Last Quarter (waning). Always use the phase field to determine which half of the lunar cycle you're in, not just the illumination percentage.

Hardcoding the lunar cycle length. The synodic month averages 29.53 days but varies between 29.27 and 29.83 days. Don't assume exactly 29.5 days when calculating future phases.

Summary#

Building a moon phase display involves fetching data from the API, understanding the eight principal phases, and choosing the right visual representation for your use case. Key implementation points:

  • Use the phase field for categorical display (emoji, icons) and illumination for precise visual rendering

  • Handle missing moonrise/moonset times gracefully

  • Cache responses since moon data for a given location and date is deterministic

  • Consider your users' hemisphere when rendering visual representations

Ready to add moon phases to your application? Get your API key and start building.

Related guides: