Add Static Files in Django

Learn how to add static files like CSS, JS, and images to your Django projects in this hands-on tutorial. Step-by-step guidance, troubleshooting, and next steps included.

Focus: add static files css js django

Sponsored

You've built Django views, wired up URL patterns, and rendered data with templates — but your pages still look like unstyled HTML from 1999. The missing piece is static files: the CSS that makes your app beautiful, the JavaScript that makes it interactive, and the images that make it feel real. In this tutorial, you'll learn how to add static files: css, js, and images in Django, using best practices that scale from a toy project to production.

The problem this lesson solves

By default, Django does not serve static files. When you reference a CSS file in a template, the browser requests it, but Django returns a 404 — unless you configure it. If you've ever seen a page render with unstyled HTML, or a broken image icon, you've hit this exact wall. This lesson eliminates that pain: you'll learn how to organize static files, reference them in templates, and serve them correctly in both development and production.

Core concept / mental model

Think of static files as the non-changing assets of your site — CSS, JavaScript, images, fonts, and PDFs. They are called static because they don't change per request, unlike your dynamic HTML templates. Django separates these from templates for two powerful reasons:

  • Performance: Static files can be served by a fast web server (like Nginx or CDN) without hitting Python at all.
  • Maintainability: Keeping CSS/JS/images in their own static/ directory makes your project structure clear and deployable.

A mental model: your Django project is a restaurant. Templates are the menu (what you present to customers), views are the kitchen (preparing dishes), and static files are the décor, table settings, and music — they make the experience pleasant but aren't part of the meal itself.

Key definitions

  • Static files: Assets like style.css, app.js, logo.png that don't change per request.
  • STATIC_URL: The URL prefix for static files, e.g., /static/.
  • STATICFILES_DIRS: A list of directories where Django looks for static files during development.
  • STATIC_ROOT: The absolute filesystem path where Django collects all static files for production using the collectstatic command.

How it works step by step

Here's the sequence Django follows when you configure static files correctly:

  1. Configure STATIC_URL in settings.py — the URL prefix for serving static files. This tells Django how to map a URL like /static/css/style.css to a file on disk.
  2. Set STATICFILES_DIRS — a list of file-system directories to search for static files. This is especially useful for project-wide assets like a global stylesheet.
  3. Create the static/ directory in your app (or project) and place files inside it. Django automatically finds static files in each app's static/ folder.
  4. Use the {% load static %} tag in templates to generate the correct URL for each asset.
  5. Run collectstatic when you deploy — this copies all static files into STATIC_ROOT, which your web server can serve directly.

Pro tip: In development, Django's built-in server serves static files automatically if your DEBUG = True and django.contrib.staticfiles is in INSTALLED_APPS (it is by default). But in production, you must run collectstatic and configure your web server — otherwise, your styles will vanish.

Hands-on walkthrough

Let's put this into practice. We'll add a global CSS file, a JavaScript file for interactivity, and an image to a simple Django project. Follow along in your terminal.

1. Set up the project structure

Ensure your project looks like this (we assume you've completed prior lessons on models, views, and templates):

myproject/
├── manage.py
├── myproject/
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── myapp/
    ├── views.py
    ├── templates/myapp/home.html
    └── static/myapp/
        ├── css/style.css
        ├── js/main.js
        └── img/logo.png

2. Update settings.py

Open myproject/settings.py and add the following near the bottom (after STATIC_URL = 'static/' which Django generates):

STATIC_URL = 'static/'
STATICFILES_DIRS = [
    BASE_DIR / "static",  # project-wide static directory
]
STATIC_ROOT = BASE_DIR / "staticfiles"  # for production
  • STATICFILES_DIRS tells Django to look in the static/ directory at the project root for files that aren't tied to a specific app.
  • STATIC_ROOT is the destination for collectstatic.

3. Create the static files

Create a project-level static/ directory and add a global CSS file:

/* static/css/style.css */
body {
    background-color: #f0f4f8;
    font-family: Arial, sans-serif;
}
.btn-primary {
    background-color: #0056b3;
    color: white;
    padding: 10px 20px;
    text-decoration: none;
}

Create a small JavaScript file that adds an alert on button click:

// static/js/main.js
document.addEventListener('DOMContentLoaded', function() {
    const button = document.querySelector('.btn-primary');
    if (button) {
        button.addEventListener('click', function() {
            alert('Button clicked!');
        });
    }
});

And add an image to static/img/ — you can use your own logo or a placeholder like logo.png.

4. Update your template

Modify myapp/templates/myapp/home.html to load and use the static files:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
    <script src="{% static 'js/main.js' %}"></script>
</head>
<body>
    <img src="{% static 'img/logo.png' %}" alt="Logo">
    <h1>Welcome to My App</h1>
    <a href="#" class="btn-primary">Click me</a>
</body>
</html>

5. Run the server and see it work

python manage.py runserver

Visit http://127.0.0.1:8000/. You should see the background color, the styled button, the logo image, and clicking the button shows an alert. If something's off, check the troubleshooting section below.

Complete example: app-level vs project-level static files

Here's a more complete scenario where we place static files inside an app:

# settings.py (partial)
STATIC_URL = 'static/'
STATICFILES_DIRS = [
    BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "staticfiles"

In your app's templates/myapp/about.html:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <p>Static files work!</p>
</body>
</html>

If you place style.css inside myapp/static/css/style.css, you must reference it as myapp/css/style.css in the template — Django namespaces static files by the app name to avoid collisions. Use {% static 'myapp/css/style.css' %}.

Pro tip: Always use the {% static %} template tag, never hardcode /static/... URLs. This way, if you change STATIC_URL later, your templates automatically update.

Compare options / when to choose what

There are several ways to manage static files, and choosing the right one depends on your project size and deployment strategy:

Option Pros Cons Best for
App-level static folders Keeps assets close to the app; Django automatically finds them Can cause naming collisions if apps have same filename Small/medium projects with per-app assets
Project-level STATICFILES_DIRS Centralized; easy to share assets across apps Requires explicit path config; extra step to set up Projects with a shared design system
CDN + whitenoise Great performance; no server config needed Requires extra dependency; not ideal for local development Production deployment
Django's built-in development serving Zero config in dev; works out of the box Not secure/performant for production Development only

When to choose what: - For learning and small apps, developer server with STATICFILES_DIRS is straightforward. - For production, use collectstatic and serve via a web server or whitenoise. - For design assets shared across your whole site, prefer project-level static dirs.

Troubleshooting & edge cases

Common issues you'll encounter when adding static files:

  1. "Static file not found" 404 in development — If DEBUG = False or you forgot django.contrib.staticfiles in INSTALLED_APPS, the dev server stops serving static files. Set DEBUG = True in development.

  2. Styles load but images don't — This often means you're using absolute URLs like /static/img/logo.png instead of the {% static %} tag. Always use the tag to generate the correct path.

  3. collectstatic shows "0 static files copied" — Your STATICFILES_DIRS might point to a non-existent directory, or your app static folder isn't in a discovered app. Verify the paths in settings.py and run python manage.py collectstatic --dry-run to see what it finds.

  4. Files load in development but break in production — This is classic. In production, you must set DEBUG = False and configure a web server to serve STATIC_ROOT. whitenoise can simplify this if you're on a platform like Heroku.

  5. Caching issues where old CSS/JS persists — Browsers cache static files aggressively. Rename files or use Django's ManifestStaticFilesStorage to append content hashes to filenames.

Pro tip: If DEBUG = False in your local environment while testing, you can still serve static files by adding django.views.static.serve to your URLconf, but that's insecure — only use it in development.

What you learned & what's next

You've just unlocked the ability to style and add interactivity to your Django apps. Specifically, you learned:

  • The core concept of static files and why they're separate from templates.
  • How to configure STATIC_URL, STATICFILES_DIRS, and STATIC_ROOT in settings.py.
  • How to create project-level and app-level static directories.
  • How to use the {% load static %} tag in templates to link CSS, JS, and images.
  • How to run collectstatic for production and avoid common pitfalls.

Next up is Forms and User Input Handling — you'll learn how to build forms that collect data, validate it, and save it to your database. Static files will make those forms look professional and user-friendly.

Now that your app looks good, it's time to make it interactive. Go ahead and experiment by adding a new CSS class or a JavaScript function to your project — the more you practice, the more confident you'll become.

Practice recap

Try this mini-exercise: add a new CSS file that changes the background color on your existing page. Then add a JavaScript alert on button click to see interactivity. Finally, run python manage.py collectstatic and inspect where files are copied — this prepares you for deployment.

Common mistakes

  • Hardcoding /static/... URLs in templates instead of using {% static %} tag — breaks when STATIC_URL changes.
  • Forgetting to set STATICFILES_DIRS when using project-level static files — Django only searches app-level folders automatically.
  • Running collectstatic without setting STATIC_ROOT — results in an error.
  • Leaving DEBUG = True in production — Django will not serve static files safely, causing 404s.

Variations

  1. Use django-storages with cloud services like S3 to serve static files directly from a CDN.
  2. Adopt whitenoise middleware to serve static files efficiently in production with zero web server configuration.
  3. Use Django's ManifestStaticFilesStorage for hashed filenames and better caching.

Real-world use cases

  • A corporate website uses a global CSS file for consistent branding across all pages.
  • An e-commerce store loads product images dynamically from the app's static folder.
  • A data dashboard includes custom JavaScript to render charts and user interactions.

Key takeaways

  • Static files are non-dynamic assets like CSS, JS, and images, served separately from templates.
  • Configure STATIC_URL, STATICFILES_DIRS, and STATIC_ROOT in settings.py for development and production.
  • Use the {% load static %} template tag to reference static files correctly.
  • In production, run collectstatic and serve files via a web server or CDN for performance.
  • Avoid naming collisions by using app-level static namespaces like myapp/css/style.css.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.