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:

1   from ludic.css import CSSProperties
2   from ludic.html import form
3   
4   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:
1   form(..., style={"background-color": "#fff"})

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

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

The styles Class Property

#

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

 1   from typing import override
 2   
 3   from ludic.attrs import ButtonAttrs
 4   from ludic.html import button
 5   from ludic.components import ComponentStrict
 6   
 7   
 8   class Button(ComponentStrict[str, ButtonAttrs]):
 9       classes = ["btn"]
10       styles = {
11           "button.btn": {
12               "background-color": "#fab",
13               "font-size": "16px",
14           }
15       }
16   
17       @override
18       def render(self) -> button:
19           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:

 1   class Button(ComponentStrict[str, ButtonAttrs]):
 2       classes = ["btn"]
 3       styles = {
 4           "button.btn": {
 5               "color": "#eee",  # type: ignore[dict-item]
 6               ".icon": {
 7                   "font-size": "16px",
 8               }
 9           }
10       }
11       ...

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

 1   from ludic.html import style
 2   
 3   
 4   class Button(ComponentStrict[str, ButtonAttrs]):
 5       classes = ["btn"]
 6       styles = style.use(lambda theme:{
 7           "button.btn": {
 8               "color": theme.colors.primary,
 9           }
10       })
11       ...

Collecting The Styles

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

    1   from ludic.html import style
    2   
    3   styles = style.load()

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

    1   <style>
    2       button.btn { background-color: #fab; font-size: 16px; }
    3   </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:

    1   from ludic.html import style
    2   
    3   from your_app.components import Button, Form
    4   
    5   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:

 1   from typing import override
 2   
 3   from ludic.attrs import NoAttrs
 4   from ludic.html import html, head, body, style
 5   from ludic.components import Component
 6   from ludic.types import AnyChildren
 7   
 8   
 9   class Page(Component[AnyChildren, NoAttrs]):
10       @override
11       def render(self) -> html:
12           return html(
13               head(
14                   style.load(cache=True)
15               ),
16               body(
17                   ...
18               ),
19           )

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:

 1   from collections.abc import AsyncIterator
 2   from contextlib import asynccontextmanager
 3   
 4   from ludic.web import LudicApp
 5   
 6   
 7   @asynccontextmanager
 8   async def lifespan(_: LudicApp) -> AsyncIterator[None]:
 9       style.load(cache=True)  # cache styles before accepting requests
10       yield
11   
12   
13   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:

 1   from ludic.html import style
 2   
 3   style(
 4       {
 5           "a": {
 6               "text-decoration": "none",
 7               "color": "red",
 8           },
 9           "a:hover": {
10               "color": "blue",
11               "text-decoration": "underline",
12           },
13       }
14   )

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

 1   from ludic.html import style
 2   
 3   style("""
 4   .button {
 5       padding: 3px 10px;
 6       font-size: 12px;
 7       border-radius: 3px;
 8       border: 1px solid #e1e4e8;
 9   }
10   """)

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:

 1   from dataclasses import dataclass
 2   
 3   from ludic.styles.types import Color
 4   from ludic.styles.themes import Colors, Fonts, Theme
 5   
 6   
 7   @dataclass
 8   class MyTheme(Theme):
 9       name: str = "my-theme"
10   
11       fonts: Fonts = field(default_factory=Fonts)
12       colors: Colors = field(
13           default_factory=lambda: Colors(
14               primary=Color("#c2e7fd"),
15               secondary=Color("#fefefe"),
16               success=Color("#c9ffad"),
17               info=Color("#fff080"),
18               warning=Color("#ffc280"),
19               danger=Color("#ffaca1"),
20               light=Color("#f8f8f8"),
21               dark=Color("#414549"),
22           )
23       )

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

1   from ludic.styles.themes import Fonts, Size, LightTheme
2   
3   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:

 1   from typing import override
 2   
 3   from ludic.attrs import ButtonAttrs
 4   from ludic.components import Component
 5   from ludic.html import button, style
 6   
 7   
 8   class Button(Component[str, ButtonAttrs]):
 9       classes = ["btn"]
10       styles = style.use(lambda theme: {
11           "button.btn:hover": {
12               "background-color": theme.colors.primary.lighten(1)
13           }
14       })
15   
16       @override
17       def render(self) -> button:
18           return button(
19               *self.children,
20               style={
21                   # Use primary color from theme
22                   "background-color": self.theme.colors.primary
23               }
24           )

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:

 1   from ludic.attrs import GlobalAttrs
 2   from ludic.styles.themes import DarkTheme, LightTheme, set_default_theme
 3   from ludic.components import Component
 4   from ludic.html import a, b, div, style
 5   
 6   dark = DarkTheme()
 7   light = LightTheme()
 8   
 9   set_default_theme(light)
10   
11   class MyComponent(Component[str, GlobalAttrs]):
12       styles = style.use(
13           # uses the theme specified by the `style.load(theme)` method
14           # or the default theme if `style.load()` was called without a theme
15           lambda theme: {
16               "#c1 a": {"color": theme.colors.warning},
17           }
18       )
19   
20       @override
21       def render(self) -> div:
22           return div(
23               # uses the local theme (dark)
24               dark.use(ButtonPrimary("Send")),
25   
26               # uses the default theme (light)
27               ButtonSecondary("Cancel"),
28   
29               # uses the default theme (light)
30               style={"background-color": self.theme.colors.primary}
31           )
32   
33   # loads styles form all components with the default theme (light)
34   my_styles = style.load()
35   
36   # loads style with the specified theme (dark)
37   my_styles = style.load(theme=dark)
Made with Ludic and HTMX and 🐍 • DiscordGitHub