audrey.feldroy.com

The experimental notebooks of Audrey M. Roy Greenfeld. This website and all its notebooks are open-source at github.com/audreyfeldroy/audrey.feldroy.com


# 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;}')

<style>:root {color-scheme: light dark;}</style>



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);}')

<style>body {background-color: light-dark(#ffffff, #1a1a1a); color: light-dark(#1a1a1a, #ffffff);}</style>



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))};""")

<style>:root {color-scheme: light dark; --lightshade: #ffffff; --darkshade: #1a1a1a;}



body {background-color: light-dark(var(--lightshade), var(--darkshade))};</style>



style((':root {color-scheme: light dark; --lightshade: #ffffff; --darkshade: #1a1a1a;}\n\nbody {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
(#41) [Path('../arg-blog-fasthtml/nbs/2023-07-29-Blogging-With-nbdev.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-12-31-Note-Box-FastTag.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-07-29-Delegates-Decorator.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-07-29-Auth-in-FastHTML.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-09-Reading-and-Writing-Jupyter-Notebooks-With-Python.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-06-Understanding-FastHTML-Headers.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-10-Understanding-FastHTML-Routes-Requests-and-Redirects.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-07-16_Xtend_Pico.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-12-28-Minimal-Typography-for-FastHTML-Apps.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-12-26-Showing-FTs-in-Jupyter-Notebooks.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-04-Claude-Artifacts-in-Notebooks.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-05-SSH-Agent-to-Save-Passphrase-Typing.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-12-27-Notebook-Names-to-Cards.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-11-NBClassic-Keyboard-Shortcuts-in-Command-and-Dual-Mode.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-01-FastHTML-Piano-Part-1.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-18-Alarm-Sounds-App.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-07-Verifying-Bluesky-Domain-in-FastHTML.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-12-24-Trying-execnb.ipynb'),Path('../arg-blog-fasthtml/nbs/2025-01-01-Command-Substitution-in-Bash.ipynb'),Path('../arg-blog-fasthtml/nbs/2024-08-05-Claudette-FastHTML.ipynb')...]
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]))

2023-07-29-Blogging-With-nbdev.ipynb

Lorem ipsum dolor sit amet.

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]))

2023-07-29-Blogging-With-nbdev.ipynb

Lorem ipsum dolor sit amet.

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])))

2023-07-29-Blogging-With-nbdev.ipynb

Lorem ipsum dolor sit amet.

2024-12-31-Note-Box-FastTag.ipynb

Lorem ipsum dolor sit amet.

2024-07-29-Delegates-Decorator.ipynb

Lorem ipsum dolor sit amet.

2024-07-29-Auth-in-FastHTML.ipynb

Lorem ipsum dolor sit amet.

2025-01-09-Reading-and-Writing-Jupyter-Notebooks-With-Python.ipynb

Lorem ipsum dolor sit amet.

2025-01-06-Understanding-FastHTML-Headers.ipynb

Lorem ipsum dolor sit amet.

2025-01-10-Understanding-FastHTML-Routes-Requests-and-Redirects.ipynb

Lorem ipsum dolor sit amet.

2024-07-16_Xtend_Pico.ipynb

Lorem ipsum dolor sit amet.

2024-12-28-Minimal-Typography-for-FastHTML-Apps.ipynb

Lorem ipsum dolor sit amet.

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

© 2024-2025 Audrey M. Roy Greenfeld