First off, before you read this, let me plainly state that I am deeply and profoundly stupid and am often predisposed towards overzealousness. It is entirely possible that everything I have written (and, in some cases, Claude has written) in this codebase is fundamentally misguided and, like its creator, very stupid.
With that being said, this system has helped me write good looking web pages in FastHTML. If you don't want to get all of your styling from a plug-and-play opinionated style/component library, or if you want to communicate a bespoke design system in Python (especially with FastHTML), this may actually be quite useful for you. It also doesn't require you to learn a whole lot of new concepts to start using (hopefully).
FastStyle provides a declarative API for defining styles with automatic CSS generation, style composition, and usage tracking in pure Python.
Well, maybe "pure" is not a useful term here. We use PascalCase in functions and we junk up modules with folding regions a lot. Pythonistas may think is dirty and uncouth. Oh well.
Anyways, enjoy!
- Type-safe CSS properties - When you type "." (provided you using modern IDE with autocomplete features), you get the benefit of being hardened CSS shaman without any of the up-front cost (cost come later)
- Design token integration - FastHTML recommends MonsterUI, but this is lots of complex systems under the hood you have to know about. And you have to use Tailwind. And if you have any opinions about what it should look like, complexity go brrrr. This easier for custom design systems.
- Style composition - You can basically apply your hard opinions about how code should be organized to your CSS. Issoke. Compose, decompose, SoC or LoB, DRY/SOLID, Clean/Dirty, whatever man. Do what feels right. Go nuts.
- Nested selectors - Support for pseudo-classes, media queries, and child selectors - call FastStyle for your "thing" (component, utility class, whatever you want my guy). Pass in ChildStyle calls for variations on/permutations of "thing". Hell, throw ChildStyle calls in for children of "thing", or mobile representation of "thing". Or don't and just do all that explicitly without any higher-order Agarthan function magic. "It's your 'thing': do what you wanna do" - The Isley Brothers
- Automatic CSS generation - Styles are collected and output as optimized CSS. Write it to a file and sideload it as a resource (spits). Be a based mega-dict chad and just yeet it into a header style tag. You actually don't need to care about which, and your users will never care about it either (they just want page make load fast see stuff sooner).
- Optimized CSS - You can very easily just not include any CSS thats not used at a per-page level. Again, write it to a file and sideload then cache, or build then cache and load in header style tag. Or don't cache who gibs fuk. Is all still pretty fast at the end of the day. And if you're using this it probably will never matter which it is.
- Python-native design tokens - Comes with its own token build system to effectively wholesale steal the magic of Style Dictionary without having to give JavaScript greasers any credit or satisfaction. Define your tokens in JSON/YAML, run
faststyle tokens build, get Python enums, CSS variables, whatever you need. - Zero runtime overhead - All CSS is generated at build time. Well, not "all". Maybe not "zero" runtime overhead. TBH Claude may have went overboard here. There might be some runtime overhead if you choose to have dynamic style definition based on user interactions. You can do that too. But default case is "all" and maybe "zero" but also probably not "zero" but still pretty quick. Is gud.
pip install faststyleHere's the simplest example that actually does something useful:
from faststyle import FastStyle, ChildStyle
# Define a button style
muh_button = FastStyle(
"btn-primary",
padding="12px 24px",
background_color="#007bff",
color="white",
border_radius="4px",
cursor="pointer",
transition="all 0.2s ease",
# Hover state
ChildStyle("&:hover",
background_color="#0056b3",
transform="translateY(-1px)"
),
# Responsive design
ChildStyle("@media (max-width: 768px)",
padding="8px 16px",
font_size="14px"
)
)
# Use in your template - it's just a string when you need it to be
html = f'<button class="{muh_button}">Click meh</button>'
# Use with FastHTML and skip template hell
button = ft.Button('Click meh', cls=muh_button)The FastStyle function is the main thing you'll use. It returns a ClassDispatch object, which sounds fancy but really just means "a thing that acts like a string when you need it to".
card = FastStyle(
"card",
background="white",
border_radius="8px",
padding="16px",
box_shadow="0 2px 4px rgba(0,0,0,0.1)"
)
# Use as string
print(card) # Output: "card"
print(f"my-{card}") # Output: "my-card"ChildStyle is how you handle all the nested stuff - hover states, media queries, child elements, whatever CSS lets you nest. It's pretty straightforward once you see it in action:
nav = FastStyle(
"nav",
display="flex",
gap="16px",
# Pseudo-class
ChildStyle("&:hover", background="rgba(0,0,0,0.05)"),
# Child element (accessible via nav.link)
ChildStyle(".link",
color="blue",
text_decoration="none"
),
# Media query with nested children
ChildStyle("@media (max-width: 768px)",
flex_direction="column",
ChildStyle(".link", padding="8px")
)
)Also, ChildStyle come along for ClassDispatch ride - you can access class names for nested ChildStyle selectors with dot notation
# Access parent selector with the ClassDispatch object returned by FastStyle
def mek_nav_menu(*links) -> ft.Div:
return ft.Div(links, cls=nav)
# Access child classes for use with chirrens
nav_menu = mek_nav_menu(ft.A('muhproducts', cls=nav.link), ft.A('muhservices', cls=nav.link))You can also make styles reference other styles to inherit their properties. This is helpful when you have base styles you want to reuse (which you may not, no h8 - this just like, an option, man):
# Base styles
text_base = FastStyle("text-base", font_size="16px", line_height="1.5")
interactive = FastStyle("interactive", cursor="pointer", user_select="none")
# Composed style
button = FastStyle(
"button",
ref=[text_base, interactive], # Inherits from multiple styles
padding="8px 16px",
background="blue",
color="white"
)If you're using a design token system (tbh this has become more trouble than its worth but I hold out hope that it will pay off one day), FastStyle comes with some conveniences to make these tokens more accessible in Python (press "." and cure all stupidity woes).
First, define your tokens in JSON (or YAML if you're that person):
// tokens.json
{
"base": {
"color": {
"blue": { "100": "#e3f2fd", "500": "#2196f3", "900": "#0d47a1" }
},
"spacing": {
"sm": "8px", "md": "16px", "lg": "24px"
}
},
"semantic": {
"color": {
"primary": "{base.color.blue.500}",
"text": { "primary": "#333", "secondary": "#666" }
}
}
}Then build your tokens:
# Generate Python classes and CSS variables
faststyle tokens build
# Or with options
faststyle tokens build --source ./design/tokens.json --output ./src/generated/Now use them in your styles:
# Auto-generated from your tokens
from generated.tokens import s, p # semantic and primitive tokens
btn_base = FastStyle(
"btn-base",
padding=f"{s.spacing.SM} {s.spacing.MD}", # Semantic tokens
background=s.color.primary,
color=p.color.blue._100, # Primitive tokens (numeric keys prefixed with _)
font_size=s.typography.body.size
)Styles get automatically registered to a global registry that handles CSS generation. This might sound overcomplicated (and maybe it is), but it means you don't have to manually collect your styles or worry about output order:
from faststyle import StyleRegistry
# Create a custom registry (optional - a default one is provided)
my_registry = StyleRegistry()
# Define styles with custom registry
header = FastStyle(
"hdr",
height="60px",
registry=my_registry
)
# Generate CSS
css = my_registry.generate_css() # All registered styles
css_optimized = my_registry.generate_css(optimized=True) # Only used stylesFastStyle can track which styles actually get used in your application. This is probably premature optimization, as I find it broadly unlikely you even need the optimization features here, but it would be dumb not to include it since I (see: Claude) did build it already anyways:
# Styles are marked as "used" when converted to string
btn_base = FastStyle("btn-base", background="blue")
# This marks the style as used
muh_button = ft.Button('Click Meh', cls=btn_base)
# Generate optimized CSS (only includes used styles)
css = registry.generate_css(optimized=True)So, for many unimportant reasons, I ended up using a graph system to track the usage of styles and be able to optimize the CSS load and avoid junking up the DOM with intermediary classes/styles (since I don't sideload CSS files). I'll be honest - this is probably overkill for most use cases, but it exists so I am going to mention it lest ye get confused by it:
from faststyle import get_style_graph
# Analyze style usage
graph = get_style_graph()
graph.analyze_usage()
# Categories:
# - 'direct': Used on elements
# - 'mixin_only': Only referenced by other styles
# - 'unused': Never usedYou can create separate registries for different contexts. The main use case I've found for this is keeping email CSS (with all its limitations) separate from your regular web styles:
web_registry = StyleRegistry()
email_registry = StyleRegistry()
# Web styles with advanced CSS
web_button = FastStyle(
"button",
background="linear-gradient(45deg, #667eea 0%, #764ba2 100%)",
backdrop_filter="blur(10px)",
registry=web_registry
)
# Email-safe styles
email_button = FastStyle(
"btn-email",
background="#667eea",
border="1px solid #5a67d8",
registry=email_registry
)There are CSS enum classes if you want that extra type safety (or just prefer autocomplete over remembering CSS values):
from faststyle import CSS
card = FastStyle(
"card",
display=CSS.display.FLEX,
flex_direction=CSS.flex_direction.COLUMN,
align_items=CSS.align_items.CENTER,
position=CSS.position.RELATIVE,
cursor=CSS.cursor.POINTER
)I also threw in the EmailCSS convenience class I use to get IDE support for "how mek email CSS that work for most email providers without learning their CSS support" - it only includes commonly supported values:
email_button=FastStyle(
"btn-email",
background="#667eea",
border=f"{EmailCSS.border_width('1px')} {EmailCSS.border_style.SOLID} #5a67d8",
registry=email_registry
)def FastStyle(
class_name: str,
*children: ChildStyle,
ref: Optional[ClassDispatch | List[ClassDispatch]] = None,
registry: Optional[StyleRegistry] = None,
**css_properties
) -> ClassDispatchParameters:
class_name: The CSS class name (without the dot prefix)*children: ChildStyle definitions for nested stylesref: Optional style(s) to inherit properties fromregistry: Custom style registry (uses global default if not provided)**css_properties: Any valid CSS properties (snake_case converts to kebab-case)
Returns:
ClassDispatchobject that acts as a string and provides child access
def ChildStyle(
selector: str,
*nested_children: ChildStyle,
ref: Optional[ClassDispatch | List[ClassDispatch]] = None,
**css_properties
) -> StyleDefParameters:
selector: CSS selector pattern that gets processed based on what it contains:"@media (...)"- Media query: wraps all nested styles"&:hover","&:focus","&.active"- The&gets replaced with parent selector".child","#id"- Selectors starting with.or#append as descendant"header","footer"- Plain strings: if it's an HTML element, uses as-is; otherwise adds.prefix"div > span",":not(:last-child)"- Complex selectors (containing>,+,~,:, etc.) used as-is
*nested_children: Further nested ChildStyle definitionsref: Optional style(s) to inherit properties from**css_properties: CSS properties for this selector
Selector Processing Examples:
card = FastStyle('card',
# & replacement
ChildStyle('&:hover', ...), # → .card:hover
ChildStyle('& .active', ...), # → .card .active
# Named children (accessible via card.header, card.body)
ChildStyle('.header', ...), # → .card .header
ChildStyle('header', ...), # → .card .header (auto-prefixed)
# HTML elements
ChildStyle('img', ...), # → .card img
ChildStyle('div > span', ...), # → .card div > span
# Complex selectors
ChildStyle('&:not(:last-child)', ...), # → .card:not(:last-child)
# Reverse selector (parent inside child)
ChildStyle('.sidebar &', ...), # → .sidebar .card
)class StyleRegistry:
def add_style(self, media_query: str, selector: str, properties: Dict[str, str])
def generate_css(self, optimized: bool = False) -> str
def clear(self)Methods:
add_style: Manually add a style rulegenerate_css: Generate CSS outputoptimized=False: Include all registered stylesoptimized=True: Only include used styles
clear: Clear all registered styles
The object returned by FastStyle:
class ClassDispatch:
# String representation
def __str__(self) -> str # Returns the class name
# Child access
def __getattr__(self, name: str) -> str # Returns child class name- Define styles at module level - They're meant to be defined once and reused, not recreated on every render
- Use design tokens - It will be easier for you to change shit (and know what shit you can use when writing other shit) if you use the design tokens. But this actually not best practice, this just a suggestion if you using bespoke design system.
- Compose, don't duplicate - Use
refto share common properties (but also don't go crazy with the abstraction - here be Dragonball Z weebery) - Organize by component - Keep your styles near the components that use them so is easier to change shit. Use a common/components.py to organize your global components that you use everywhere. Use page/user flow branch modules to define one-off components that only get used on that page/in that user interaction flow. In any case, write your styles above your components (on god is much easier, even if they long).
- Use the CSS enums - Or don't, I'm not your dad (I'm pretty sure). But the autocomplete mek life real gud.
- Don't make new files, use folds This just a general principle that has helped me when working with FastHTML and this design framework, but ultimately life has been a lot easier for me when I avoid defining new modules. If a file gets too big for you to comprehend, use folding to organize shit. Got 50 styles above 6 different related component factories? Cool. Use custom folding regions to section off the component type, then use other custom folding regions to section off styles vs components:
# ===== FUNCTIONS ===== #
## ===== COMPONENTS ===== ##
### ===== BUTTONS ===== ###
#!> Styles
#!> Base button styles
btn_base = FastStyle(...)
btn_disabled = FastStyle(...)
#!<
#!> Primary button styles
...
#!<
...
#!< end of Styles
#!> Factories
def PrimaryButton(...) -> ft.Button: return ft.Button(..., cls=f'{btn_base} {btn_primary} {btn_lg}')
def GhostButton(...) -> ft.Button: return ft.Button(..., cls=f'{btn_base} {btn_ghost} {btn_sm}')
#!<Now, is this a "best practice" with respect to this specific library? No, this an oddly specific opinion about programming, largely detached from the purpose of this README. But, it has helped me immensely to be fearful of small files and the masculine urge to create lots of them. So, I include it here because I don't have a blog or a YouTube channel (at least, not about programming). I hope it helps you as it has helped me (to mek unnasten code gud).
from fasthtml import Div, Button
from faststyle import FastStyle
# Define styles
button_style = FastStyle(
"btn",
padding="8px 16px",
background="#007bff",
color="white",
border_radius="4px"
)
# Use in components
def MyButton(text, **props):
return Button(text, cls=button_style, **props)# styles.py
from faststyle import FastStyle, get_default_registry
button = FastStyle("btn", padding="8px 16px", background="#007bff")
def get_styles_css():
return get_default_registry().generate_css()
# views.py
def my_view(request):
context = {
'styles_css': get_styles_css(),
'button_class': str(button)
}
return render(request, 'template.html', context)from flask import Flask, render_template_string
from faststyle import FastStyle, get_default_registry
app = Flask(__name__)
# Define styles
card = FastStyle("card", padding="16px", background="white", border_radius="8px")
@app.context_processor
def inject_styles():
return {
'styles_css': get_default_registry().generate_css(),
'card_class': str(card)
}FastStyle includes a Python-native token builder (no JavaScript I kno is radical rite?):
# Basic usage - looks for tokens.json in current dir
faststyle tokens build
# With options
faststyle tokens build \
--source ./design/tokens.yml \
--output ./src/generated/ \
--format css,pythonOr use the Python API if you no likey CLI:
from faststyle.tokens import TokenBuilder
builder = TokenBuilder('./tokens.json')
builder.add_output('css', './generated/tokens.css')
builder.add_output('python', './generated/tokens.py')
builder.build()Tokens follow a simple structure. Use base for primitives and semantic for... semantic tokens:
{
"base": {
"color": {
"gray": {
"100": { "value": "#f7f7f7" },
"500": { "value": "#888888" },
"900": { "value": "#111111" }
}
}
},
"semantic": {
"color": {
"text": {
"primary": { "value": "{base.color.gray.900}" },
"secondary": { "value": "{base.color.gray.500}" }
}
}
}
}References (the {base.color.gray.900} things) get resolved automatically. It's like variables but with more steps.
You can also add your own third tier if you deranged carpenter (i.e. primitive + alias + mapped, base + semantic + component, etc.).
Running faststyle tokens build creates:
-
tokens.css - CSS custom properties
:root { --color-text-primary: #111111; --color-text-secondary: #888888; }
-
tokens.py - Python enums and classes
# Semantic tokens (use these mostly) class Semantics: class color: TEXT_PRIMARY = "var(--color-text-primary)" TEXT_SECONDARY = "var(--color-text-secondary)" s = Semantics() # Primitive tokens (when you need the raw values) class Primitives: class color: class gray: _100 = "#f7f7f7" # Numeric keys prefixed with _ _500 = "#888888" _900 = "#111111" p = Primitives() # Add to generated/__init__.py from semantics import s from primitives import p __all__ = ['p', 's'] # Then you just import: from generated import p, s
If you need custom transforms or outputs (likely):
from faststyle.tokens import TokenBuilder, Transform
# Custom transform
def add_pixel_units(token):
if token.get('type') == 'spacing' and isinstance(token['value'], (int, float)):
token['value'] = f"{token['value']}px"
return token
# Custom formatter
def format_scss_map(tokens):
# Generate SCSS map format
return "$tokens: (...);"
# Use them
builder = TokenBuilder('./tokens.json')
builder.add_transform(add_pixel_units)
builder.add_output(format_scss_map, './generated/_tokens.scss')
builder.build()If you find this useful and want to contribute, I'm honestly shocked and hey man, thats pretty cool. Welcome aboard. If you find bugs... well, yeah. I figured that would probably happen. Gosh, yknow... jeez, like... man...
See Contributing Guide for details.
MIT License - see LICENSE for details. Basically, do whatever you want with it, just don't blame me if it breaks (likely).