# Weather Chip

> Open-Meteo weather chip — no API key required.

A shadcn/ui block from the [@atroui](https://www.atroui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/atroui/weather-chip).

- Page, with a live preview: https://shadcn.exchange/r/atroui/weather-chip
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/atroui/weather-chip-light-18a6060b37e6a402.png
- Registry item: https://www.atroui.com/r/weather-chip.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.atroui.com/r/weather-chip.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@atroui/utils`

## Files

### `registry/default/blocks/weather-chip.tsx`

```tsx
"use client"

import { useEffect, useState } from "react"

import { cn } from "@/lib/utils"

/** Edit lat/lon after install. Open-Meteo — no API key. */
const CONTENT = {
  lat: 40.7128,
  lon: -74.006,
  label: "Local",
}

type WeatherData = {
  temperature: number
  feelsLike: number
  weatherCode: number
  isDay: boolean
}

function weatherGlyph(code: number, isDay = true): string {
  if (code === 0) return isDay ? "☀️" : "🌙"
  if (code <= 3) return "⛅"
  if (code <= 48) return "🌫️"
  if (code <= 67) return "🌧️"
  if (code <= 77) return "🌨️"
  if (code <= 82) return "🌦️"
  if (code <= 86) return "❄️"
  if (code <= 99) return "⛈️"
  return "🌡️"
}

function useWeather(lat: number | null, lon: number | null) {
  const [data, setData] = useState<WeatherData | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    if (lat === null || lon === null) return

    let cancelled = false

    const fetchWeather = async () => {
      setIsLoading(true)
      setError(null)
      try {
        const response = await fetch(
          `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,apparent_temperature,weather_code,is_day`
        )
        if (!response.ok) throw new Error("Failed to fetch weather data")
        const result = await response.json()
        if (cancelled) return
        setData({
          temperature: Math.round(result.current.temperature_2m),
          feelsLike: Math.round(result.current.apparent_temperature),
          weatherCode: result.current.weather_code,
          isDay: result.current.is_day === 1,
        })
      } catch (err: unknown) {
        if (cancelled) return
        setError(err instanceof Error ? err.message : "Weather unavailable")
      } finally {
        if (!cancelled) setIsLoading(false)
      }
    }

    fetchWeather()
    return () => {
      cancelled = true
    }
  }, [lat, lon])

  return { data, error, isLoading }
}

export function WeatherChip({
  lat = CONTENT.lat,
  lon = CONTENT.lon,
  label = CONTENT.label,
  className,
}: {
  lat?: number
  lon?: number
  label?: string
  className?: string
} = {}) {
  const { data: weather, isLoading, error } = useWeather(lat, lon)

  if (isLoading) {
    return (
      <span
        className={cn(
          "inline-flex animate-pulse items-center gap-1 font-mono text-[11.5px] text-muted-foreground tabular-nums",
          className
        )}
        aria-label="Weather loading"
      >
        ···
      </span>
    )
  }

  if (error || !weather) {
    return (
      <span
        className={cn(
          "inline-flex items-center gap-1 font-mono text-[11.5px] text-muted-foreground tabular-nums",
          className
        )}
        aria-label="Weather unavailable"
        title="Weather offline"
      >
        —
      </span>
    )
  }

  const glyph = weatherGlyph(weather.weatherCode, weather.isDay)

  return (
    <span
      className={cn(
        "inline-flex items-center gap-1 font-mono text-[11.5px] text-muted-foreground tabular-nums",
        className
      )}
      aria-label={`Weather in ${label}, ${weather.temperature} degrees`}
      title={`${label} · feels ${weather.feelsLike}°`}
    >
      <span aria-hidden>{glyph}</span>
      <span className="text-foreground">{weather.temperature}°</span>
    </span>
  )
}
```
