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:
- The
style
HTML Attribute - The
styles
Class Property - 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 aTypedDict
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))
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 stylesTargeted 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:
- Use the
ludic.catalog.pages.HtmlPage
component which loads the styles automatically. - 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:
- subclass
Theme
base class and define the theme's attributes - 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:
- use the component's
theme
attribute - call
style.use(lambda theme: { ... })
on the component'sstyles
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:
- use the
theme.use()
method to switch theme in a component - use the
set_default_theme()
method to switch theme globally - 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)