Want to build a sports scoreboard for your website or app? This tutorial walks you through creating an NFL scoreboard viewer using React, TypeScript, and the BALLDONTLIE API. You'll learn how to fetch real-time NFL game data, display live scores, and filter games by week for the 2025 NFL season.

Whether you're building a fantasy football companion app, a sports betting dashboard, or just want to track NFL scores programmatically, this guide covers everything you need to get started with sports data APIs.

New to the API? Start with our Getting Started guide first to set up your account and API key.

What We're Building

A single-page React app that:

  • Fetches NFL games and scores from the BALLDONTLIE sports API
  • Displays live game scores with team names and venues
  • Lets users filter games by week (weeks 1-18 for the regular season)
  • Shows game status (scheduled, in progress, or final)

React NFL scoreboard app showing live game scores with team names, scores, and game status badges

Prerequisites

  • Node.js 18 or higher
  • A BALLDONTLIE API key (get one at app.balldontlie.io)
  • Basic familiarity with React and TypeScript

Step 1: Create the Project

We'll use Vite to scaffold a React TypeScript project, then add Chakra UI for styling:

npm create vite@latest nfl-scoreboard -- --template react-ts
cd nfl-scoreboard
npm install
npm install @chakra-ui/react @emotion/react @emotion/styled

Step 2: Set Up the Provider

Update src/main.tsx to wrap your app with the Chakra provider:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import App from "./App.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ChakraProvider value={defaultSystem}>
      <App />
    </ChakraProvider>
  </StrictMode>
);

Step 3: Define the Types

The BALLDONTLIE API returns structured JSON with nested team objects. Add these TypeScript interfaces to the top of src/App.tsx:

interface Team {
  id: number;
  conference: string;
  division: string;
  location: string;
  name: string;
  full_name: string;
  abbreviation: string;
}

interface Game {
  id: number;
  visitor_team: Team;
  home_team: Team;
  summary: string;
  venue: string;
  week: number;
  date: string;
  season: number;
  postseason: boolean;
  status: string;
  home_team_score: number;
  home_team_q1: number | null;
  home_team_q2: number | null;
  home_team_q3: number | null;
  home_team_q4: number | null;
  home_team_ot: number | null;
  visitor_team_score: number;
  visitor_team_q1: number | null;
  visitor_team_q2: number | null;
  visitor_team_q3: number | null;
  visitor_team_q4: number | null;
  visitor_team_ot: number | null;
}

interface GamesResponse {
  data: Game[];
  meta: {
    next_cursor: number | null;
    per_page: number;
  };
}

These types match the response from GET /nfl/v1/games. The API provides quarter-by-quarter scores, overtime scores, and full team details including conference and division.

Step 4: Build the Game Card Component

Create a component that displays a single game's score. This goes in src/App.tsx:

import {
  Box,
  VStack,
  HStack,
  Card,
  Text,
  Badge,
} from "@chakra-ui/react";

function GameCard({ game }: { game: Game }) {
  const isFinished = game.status === "Final";
  const homeWon = game.home_team_score > game.visitor_team_score;
  const awayWon = game.visitor_team_score > game.home_team_score;

  const formatDate = (dateStr: string) => {
    const date = new Date(dateStr);
    return date.toLocaleDateString("en-US", {
      weekday: "short",
      month: "short",
      day: "numeric",
      hour: "numeric",
      minute: "2-digit",
    });
  };

  return (
    <Card.Root width="100%" bg="gray.800" borderColor="gray.700">
      <Card.Body>
        <VStack gap={3} align="stretch">
          <HStack justify="space-between">
            <Text fontSize="sm" color="gray.400">
              {formatDate(game.date)}
            </Text>
            <Badge colorPalette={isFinished ? "green" : "blue"}>
              {game.status}
            </Badge>
          </HStack>

          <HStack justify="space-between" align="center">
            <VStack align="start" gap={0}>
              <Text
                fontWeight={awayWon && isFinished ? "bold" : "normal"}
                fontSize="lg"
                color="white"
              >
                {game.visitor_team.abbreviation}
              </Text>
              <Text fontSize="xs" color="gray.400">
                {game.visitor_team.full_name}
              </Text>
            </VStack>
            <Text fontSize="2xl" fontWeight="bold" color="white">
              {game.visitor_team_score}
            </Text>
          </HStack>

          <HStack justify="space-between" align="center">
            <VStack align="start" gap={0}>
              <Text
                fontWeight={homeWon && isFinished ? "bold" : "normal"}
                fontSize="lg"
                color="white"
              >
                {game.home_team.abbreviation}
              </Text>
              <Text fontSize="xs" color="gray.400">
                {game.home_team.full_name}
              </Text>
            </VStack>
            <Text fontSize="2xl" fontWeight="bold" color="white">
              {game.home_team_score}
            </Text>
          </HStack>

          <Text fontSize="xs" color="gray.500">
            {game.venue}
          </Text>
        </VStack>
      </Card.Body>
    </Card.Root>
  );
}

The component uses a dark theme with gray.800 cards on a gray.950 background. It highlights the winning team's name in bold once the game is final, and displays the game status as a colored badge.

Step 5: Create the Week Selector

Chakra UI's Select component needs a collection of items. The NFL regular season has 18 weeks:

import { Select, createListCollection } from "@chakra-ui/react";

const weeks = createListCollection({
  items: Array.from({ length: 18 }, (_, i) => ({
    label: `Week ${i + 1}`,
    value: String(i + 1),
  })),
});

Step 6: Build the Main App Component

Now wire everything together. The main component fetches games when the selected week changes:

import { useState, useEffect } from "react";
import {
  Box,
  Container,
  Heading,
  Text,
  VStack,
  HStack,
  Card,
  Spinner,
  Select,
  createListCollection,
  Badge,
} from "@chakra-ui/react";

function App() {
  const [games, setGames] = useState<Game[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [selectedWeek, setSelectedWeek] = useState("14");

  useEffect(() => {
    const fetchGames = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(
          `https://api.balldontlie.io/nfl/v1/games?seasons[]=2025&weeks[]=${selectedWeek}`,
          {
            headers: {
              Authorization: "YOUR_API_KEY",
            },
          }
        );

        if (!response.ok) {
          throw new Error("Failed to fetch games");
        }

        const data: GamesResponse = await response.json();
        const sortedGames = data.data.sort(
          (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
        );
        setGames(sortedGames);
      } catch (err) {
        setError(err instanceof Error ? err.message : "An error occurred");
      } finally {
        setLoading(false);
      }
    };

    fetchGames();
  }, [selectedWeek]);

  return (
    <Box minH="100vh" bg="gray.950" py={8}>
      <Container maxW="container.md">
        <VStack gap={6} align="stretch">
          <VStack gap={2}>
            <Heading size="2xl" color="white">
              NFL Scoreboard
            </Heading>
            <Text color="gray.400">2025 Season</Text>
          </VStack>

          <Select.Root
            collection={weeks}
            value={[selectedWeek]}
            onValueChange={(e) => setSelectedWeek(e.value[0])}
            size="lg"
            positioning={{ sameWidth: true }}
          >
            <Select.Trigger
              bg="gray.800"
              borderColor="gray.600"
              color="white"
              _hover={{ bg: "gray.700" }}
            >
              <Select.ValueText placeholder="Select week" />
            </Select.Trigger>
            <Select.Positioner>
              <Select.Content bg="gray.800" borderColor="gray.600">
                {weeks.items.map((week) => (
                  <Select.Item
                    key={week.value}
                    item={week}
                    bg="gray.800"
                    color="white"
                    _hover={{ bg: "gray.700" }}
                    _highlighted={{ bg: "gray.700" }}
                  >
                    {week.label}
                  </Select.Item>
                ))}
              </Select.Content>
            </Select.Positioner>
          </Select.Root>

          {loading && (
            <VStack py={8}>
              <Spinner size="xl" color="blue.500" />
              <Text color="gray.400">Loading games...</Text>
            </VStack>
          )}

          {error && (
            <Box p={4} bg="red.900" borderRadius="md">
              <Text color="red.200">{error}</Text>
            </Box>
          )}

          {!loading && !error && games.length === 0 && (
            <Box p={4} bg="gray.800" borderRadius="md">
              <Text color="gray.400" textAlign="center">
                No games found for this week.
              </Text>
            </Box>
          )}

          {!loading && !error && games.length > 0 && (
            <VStack gap={4} align="stretch">
              {games.map((game) => (
                <GameCard key={game.id} game={game} />
              ))}
            </VStack>
          )}
        </VStack>
      </Container>
    </Box>
  );
}

export default App;

Replace YOUR_API_KEY with your actual BALLDONTLIE API key.

Step 7: Run the App

Start the development server:

npm run dev

Open http://localhost:5173 in your browser. You should see a list of NFL games for the selected week. Use the dropdown to switch between weeks.

Understanding the API Response

The /nfl/v1/games endpoint accepts these query parameters:

  • seasons[] - Filter by season year (e.g., 2025)
  • weeks[] - Filter by week number (1-18 for regular season)
  • team_ids[] - Filter by specific teams
  • postseason - Set to true for playoff games
  • dates[] - Filter by specific dates

Each game object includes:

  • Team details with conference and division
  • Quarter-by-quarter scores
  • Overtime scores (if applicable)
  • Game venue
  • Status (scheduled time, "1st Qtr", "Halftime", "Final", etc.)

Extending the Scoreboard

This basic scoreboard can be extended in several ways:

  • Add quarter-by-quarter scores: The API returns home_team_q1, home_team_q2, etc. Display these in a table format.
  • Auto-refresh for live games: Add a polling interval during game time to update scores automatically.
  • Team logos: Store team logos locally and display them based on the abbreviation field.
  • Filter by team: Add a team selector that uses the team_ids[] parameter.

API Endpoints Used

This tutorial uses the NFL games endpoint:

GET https://api.balldontlie.io/nfl/v1/games

The BALLDONTLIE API provides comprehensive NFL data including:

  • Player statistics - Passing, rushing, receiving stats for every game
  • Team standings - Division and conference standings updated in real-time
  • Betting odds - Lines from major sportsbooks including spreads, totals, and moneylines
  • Advanced stats - Detailed rushing, passing, and receiving analytics
  • Play-by-play data - Every play from every game

Check the full documentation for all available endpoints across NFL, NBA, MLB, NHL, EPL, WNBA, NCAAF, and NCAAB.

Next Steps

Now that you have a working NFL scoreboard, try these related tutorials:

Ready to build your own sports app? Sign up for a free API key and start building today.

Need help? Join our Discord community or check the documentation.