Ludic Logo
Edit

Styles and Themes

There are two main ways of how to change the look and feel of your apps built with the Ludic framework:

  • CSS Styling – you can apply custom styles to your components.
  • Themes – you can change the colors, widths, fonts, etc. of your components.

CSS Styling

#

There are three primary ways to apply CSS properties to components within your application:

  1. The style HTML Attribute
  2. The styles Class Property
  3. The style HTML Element

The style HTML Attribute

#

You can directly embed styles within an HTML element using the style attribute. Here's an example:

from ludic.css import CSSProperties
from ludic.html import form

form(..., style=CSSProperties(color="#fff"))
  • The CSSProperties class is a TypedDict for convenience since type checkers can highlight unknown or incorrect usage.
  • You can also use a regular Python dictionary, which might be better in most cases since CSS properties often contain hyphens:
form(..., style={"background-color": "#fff"})

Note that you probably want to specify the color using a theme as you can read more about bellow.

form(..., style={"background-color": theme.colors.white})

The styles Class Property

#

Define CSS properties within your component using the styles class property. Example:

from typing import override

from ludic.attrs import ButtonAttrs
from ludic.html import button
from ludic.components import ComponentStrict


class Button(ComponentStrict[str, ButtonAttrs]):
    classes = ["btn"]
    styles = {
        "button.btn": {
            "background-color": "#fab",
            "font-size": "16px",
        }
    }

    @override
    def render(self) -> button:
        return button(self.children[0], **self.attrs_for(button))
Notice the classes attribute
The classes attribute contains the list of classes that will be applied to the component when rendered (they will be appended if there are any other classes specified by the class_ attribute).

In this case, you need to make sure you collect and render the styles. See Collecting The Styles and Integration In a Page Component.

It is also possible to nest styles similar to how you would nest them in SCSS. The only problem is that you might get typing errors if you are using mypy or pyright:

class Button(ComponentStrict[str, ButtonAttrs]):
    classes = ["btn"]
    styles = {
        "button.btn": {
            "color": "#eee",  # type: ignore[dict-item]
            ".icon": {
                "font-size": "16px",
            }
        }
    }
    ...

Again, you probably want to use themes to assign colors (we'll talk about themes later):

from ludic.html import style


class Button(ComponentStrict[str, ButtonAttrs]):
    classes = ["btn"]
    styles = style.use(lambda theme:{
        "button.btn": {
            "color": theme.colors.primary,
        }
    })
    ...

Collecting The Styles

#
  • Load Styles: Use the style.load() method to gather styles from all components in your project. This generates a <style> element:

    from ludic.html import style
    
    styles = style.load()
    

    The styles variable now renders as a <style> element with the content similar to this:

    <style>
        button.btn { background-color: #fab; font-size: 16px; }
    </style>
    

    You can also pass styles.load(cache=True) to cache the styles

  • Targeted Loading: For more control, use style.from_components(...) to load styles from specific components:

    from ludic.html import style
    
    from your_app.components import Button, Form
    
    styles = style.from_components(Button, Form)
    

Integration In a Page Component

#

You need to load the styles and render them in an HTML document. There are two options how to do that:

  1. Use the ludic.catalog.pages.HtmlPage component which loads the styles automatically.
  2. Create a new Page component and use the style.load() method manually.

The first method is described in the catalog section of the documentation. Here is the second method:

from typing import override

from ludic.attrs import NoAttrs
from ludic.html import html, head, body, style
from ludic.components import Component
from ludic.types import AnyChildren


class Page(Component[AnyChildren, NoAttrs]):
    @override
    def render(self) -> html:
        return html(
            head(
                style.load(cache=True)
            ),
            body(
                ...
            ),
        )

Caching The Styles

#

As mentioned before, passing cache=True to style.load caches loaded elements' styles during the first render. The problem is that the first request to your application renders the styles without the cache, so the response is a bit slower. If you want to cache the styles before your component even renders for the first time, you can use the lifespan argument of the LudicApp class:

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from ludic.web import LudicApp


@asynccontextmanager
async def lifespan(_: LudicApp) -> AsyncIterator[None]:
    style.load(cache=True)  # cache styles before accepting requests
    yield


app = LudicApp(lifespan=lifespan)

You can read more about Lifespan in Starlette's documentation.

The style HTML Element

#

You can also directly embed styles within a Page component using the style element. Here's an example:

from ludic.html import style

style(
    {
        "a": {
            "text-decoration": "none",
            "color": "red",
        },
        "a:hover": {
            "color": "blue",
            "text-decoration": "underline",
        },
    }
)

It is also possible to pass raw CSS styles as a string:

from ludic.html import style

style("""
.button {
    padding: 3px 10px;
    font-size: 12px;
    border-radius: 3px;
    border: 1px solid #e1e4e8;
}
""")

Themes

#

You can think of themes as an option to create CSS variables, but with typing support and more flexibility.

Ludic has two built-in themes:

  • ludic.styles.themes.LightTheme – the default theme.
  • ludic.styles.themes.DarkTheme – the dark theme.

How to Style Components Using Themes

#

Themes provide a centralized way to manage the look and feel of your components. You can directly access a component's theme to customize its styling based on your theme's settings. Here's a breakdown of how this works:

  • Theme Definition: A theme holds predefined styles like colors, fonts, and spacing. You usually define your theme separately.
  • Accessing the Theme: Components can access the current theme through a special theme attribute. This gives you direct access to your theme's values.
  • Switching Theme: Components can switch to a different theme by passing the componentto the theme.use() method. You can also switch theme globally.

Theme Definition

#

You have two options how to create a new theme:

  1. subclass Theme base class and define the theme's attributes
  2. instantiate an existing theme and override its attributes

Here is an example of the first approach:

from dataclasses import dataclass

from ludic.styles.types import Color
from ludic.styles.themes import Colors, Fonts, Theme


@dataclass
class MyTheme(Theme):
    name: str = "my-theme"

    fonts: Fonts = field(default_factory=Fonts)
    colors: Colors = field(
        default_factory=lambda: Colors(
            primary=Color("#c2e7fd"),
            secondary=Color("#fefefe"),
            success=Color("#c9ffad"),
            info=Color("#fff080"),
            warning=Color("#ffc280"),
            danger=Color("#ffaca1"),
            light=Color("#f8f8f8"),
            dark=Color("#414549"),
        )
    )

You can also instantiate an existing theme and override its attributes:

from ludic.styles.themes import Fonts, Size, LightTheme

theme = LightTheme(fonts=Fonts(serif="serif", size=Size(1, "em")))

Accessing The Theme

#

There are two ways to access the theme:

  1. use the component's theme attribute
  2. call style.use(lambda theme: { ... }) on the component's styles class attribute

Here is an example combining both approaches:

from typing import override

from ludic.attrs import ButtonAttrs
from ludic.components import Component
from ludic.html import button, style


class Button(Component[str, ButtonAttrs]):
    classes = ["btn"]
    styles = style.use(lambda theme: {
        "button.btn:hover": {
            "background-color": theme.colors.primary.lighten(1)
        }
    })

    @override
    def render(self) -> button:
        return button(
            *self.children,
            style={
                # Use primary color from theme
                "background-color": self.theme.colors.primary
            }
        )

Switching Theme

#

You can switch the theme globally or for a specific component:

  1. use the theme.use() method to switch theme in a component
  2. use the set_default_theme() method to switch theme globally
  3. use the style.load() to render styles from loaded components with a different theme

Here are some examples:

from ludic.attrs import GlobalAttrs
from ludic.styles.themes import DarkTheme, LightTheme, set_default_theme
from ludic.components import Component
from ludic.html import a, b, div, style

dark = DarkTheme()
light = LightTheme()

set_default_theme(light)

class MyComponent(Component[str, GlobalAttrs]):
    styles = style.use(
        # uses the theme specified by the `style.load(theme)` method
        # or the default theme if `style.load()` was called without a theme
        lambda theme: {
            "#c1 a": {"color": theme.colors.warning},
        }
    )

    @override
    def render(self) -> div:
        return div(
            # uses the local theme (dark)
            dark.use(ButtonPrimary("Send")),

            # uses the default theme (light)
            ButtonSecondary("Cancel"),

            # uses the default theme (light)
            style={"background-color": self.theme.colors.primary}
        )

# loads styles form all components with the default theme (light)
my_styles = style.load()

# loads style with the specified theme (dark)
my_styles = style.load(theme=dark)
Made with Ludic and HTMX and 🐍 • DiscordGitHub