Dark and Light Mode in FastHTML

by Audrey M. Roy Greenfeld | Mon, Jan 20, 2025

How to make a website check the user's preferred mode and set the background appropriately.


from fasthtml.common import *
from fasthtml.jupyter import *
from fastcore.utils import *

Compare a Page in Both Modes

Open your website in 2 Chrome profiles, one with light and one with dark mode. For example, in mine I noticed:

This told me that I need to modify the page background styles depending on the preferred mode.

Try Emulating Preference Manually

In Chrome, go to Option Cmd c to open "Inspect Elements" > Elements > Styles. Click the upside-down paintbrush icon to bring up a menu of:

To Update a FastHTML main.py With color-scheme

Step 1: Enable light-dark Support

To enable support for the CSS light-dark() color function, you set this on :root:

Style(':root {color-scheme: light dark;}')

This allows your site to respect the user's light or dark mode preference.

Step 2: Use the light-dark Function

Now wherever you specify CSS colors, you can specify a pair of colors.

Style('body {background-color: light-dark(#ffffff, #1a1a1a); color: light-dark(#1a1a1a, #ffffff);}')

Here in this example:

If you use these values a lot, define CSS variables --light-* and --dark-*

Step 3: Factor Out CSS Variables (Optional)

If you find yourself repeating color constants a lot, define CSS variables in :root and use them in light-dark() like:

Style(""":root {color-scheme: light dark; --lightshade: #ffffff; --darkshade: #1a1a1a;}

body {background-color: light-dark(var(--lightshade), var(--darkshade))};""")

Example

Here I create a component with light-dark colors. The component respects light/dark mode preference. I need stuff to put into cards, so I'm grabbing my notebooks:

nbs = L(Path('../arg-blog-fasthtml/nbs').glob('*.ipynb'))
nbs
def LightDarkNotebookCard(nb):
    "A card showing notebook info"
    return Div(H3(nb.name), P("Lorem ipsum dolor sit amet."), style="border: 3px solid light-dark(#eaeaea, #111111); padding: 10px; margin: 2px; border-radius: 4px; background-color: light-dark(#eeeeee, #1a1a1a); color: light-dark(#1a1a1a, #eeeeee);")
show(LightDarkNotebookCard(nbs[0]))

It's nice to create a rough component, then use AI to iterate on the design. Here I pasted my code into Claude with the prompt:

Make these FastHTML cards look better. No React or Tailwind, please.

That gave me this improved card FT component:

def LightDarkNotebookCard(nb):
    "A card showing notebook info with enhanced styling"
    return Div(
        H3(nb.name, style="margin: 0 0 12px 0; font-size: 1.3em;"),
        P("Lorem ipsum dolor sit amet.", 
          style="margin: 0; line-height: 1.5; opacity: 0.9;"),
        style="""
            border: 1px solid light-dark(#e0e0e0, #333333);
            padding: 20px;
            margin: 8px;
            border-radius: 8px;
            background-color: light-dark(#ffffff, #222222);
            color: light-dark(#1a1a1a, #ffffff);
            box-shadow: 0 2px 4px light-dark(rgba(0,0,0,0.05), rgba(0,0,0,0.2));
            transition: all 0.2s ease;
            cursor: pointer;
        """,
        onmouseover="""
            this.style.transform = 'translateY(-2px)';
            this.style.boxShadow = '0 4px 8px light-dark(rgba(0,0,0,0.1), rgba(0,0,0,0.3))';
        """,
        onmouseout="""
            this.style.transform = 'translateY(0)';
            this.style.boxShadow = '0 2px 4px light-dark(rgba(0,0,0,0.05), rgba(0,0,0,0.2))';
        """
    )

show(LightDarkNotebookCard(nbs[0]))

Here I map them to a handful of list items, to make them look more realistic:

def LightDarkList(nbs):
    "FT component that returns a list of notebooks"
    return Div(*nbs.map(LightDarkNotebookCard), style="columns: 3;")
show(to_xml(LightDarkList(nbs[:9])))

FastHTML App

You can see this component in action by creating a little FastHTML app with this :root style:

app, rt = fast_app(hdrs=(picolink, Style(':root {color-scheme: light dark;}')))
@rt
def index():
    return Main(H1("My Site"), LightDarkList(nbs[:9]))

I run this app from my notebook via the next line. If using a main.py, replace it with serve().

# server = JupyUvi(app)
# server.stop()

Takeaways

  1. Set color-scheme: light dark in :root to respect the user's dark/light mode preference.
  2. Use the light-dark() function to set properties based on mode
  3. Refactor colors into CSS variables if needed