Styling#
ngapp provides a Python-native styling system that replaces raw CSS strings
with composable, reusable objects. The module lives at ngapp.style and
exports four classes: Style,
Theme, CssClass, and
StyleSheet.
Tip
Prefer Quasar utilities for standard layout and spacing. Quasar already
provides classes for padding (q-pa-md), margin (q-mt-sm), flex
layout (row, col), visibility (ui_hidden), and colors
(bg-primary, text-grey-7). Use Style and StyleSheet for
things Quasar doesn’t cover: custom overlays, app-specific panel layouts,
domain-specific visual treatments, and shared multi-property styles that
have no Quasar equivalent.
Quick Example#
A fixed-position status indicator:
from ngapp.style import Style, Theme, StyleSheet
from ngapp.components import Div
theme = Theme(
primary="#164d7d",
muted="#78909c",
border="#e0e0e0",
font_sm="0.75rem",
spacing=(0, 4, 8, 12, 16, 20, 24, 32),
)
css = StyleSheet()
# Fixed overlay
status_indicator = css.add(Style(
position="fixed",
bottom="20px",
left="50%",
transform="translateX(-50%)",
background="rgba(15, 23, 42, 0.92)",
backdrop_filter="blur(4px)",
color="white",
padding="8px 20px",
border_radius="8px",
box_shadow="0 4px 12px rgba(0,0,0,0.3)",
z_index=9999,
font_size=theme.font_sm,
display="flex",
align_items="center",
gap="8px",
))
Div("Solving…", ui_class=status_indicator)
css.inject(app)
Style — Composable CSS Property Bag#
Style turns Python keyword arguments into CSS properties.
Underscores become hyphens (font_size → font-size).
from ngapp.style import Style
overlay = Style(
position="fixed",
bottom="20px",
background="rgba(0,0,0,0.8)",
backdrop_filter="blur(4px)",
color="white",
z_index=9999,
)
Merging with |:
The | operator merges two styles. The right side wins on conflicts,
matching Python dict | semantics (PEP 584). Set a value to None to
remove a property.
panel = Style(
border_right="1px solid #e0e0e0",
height="100%",
overflow_y="auto",
)
# Left panel: same but border on the other side
panel_left = panel | Style(border_right=None, border_left="1px solid #e0e0e0")
Using with components:
Style objects work directly with ui_style — they are converted to
strings automatically. Use this for truly dynamic, per-instance values:
# Dynamic color from a color picker — must be inline
swatch.ui_style = Style(background_color=self.to_hex_string())
# Per-instance grid placement
cell.ui_style = Style(grid_column=f"{col} / span 2", grid_row=str(row))
Note
Integer values are not automatically converted to pixels. Write
padding="8px" instead of padding=8. This avoids ambiguity with
unitless properties like z_index and font_weight.
Theme — Design Tokens#
Theme is a simple namespace for centralizing colors,
spacing, and font sizes. This avoids scattering magic hex values and pixel
numbers across files.
from ngapp.style import Theme
theme = Theme(
# Quasar brand colors — applied to the app via theme.apply()
primary="#164d7d",
secondary="#93B1D4",
accent="#14B8A6",
positive="#16A34A",
negative="#DC2626",
info="#0EA5E9",
warning="#F59E0B",
# App-specific tokens — used in Style() and StyleSheet
muted="#78909c",
surface="#f5f7fa",
border="#e0e0e0",
font_sm="0.75rem",
font_md="0.85rem",
spacing=(0, 4, 8, 12, 16, 20, 24, 32),
)
theme.primary # → "#164d7d"
theme.font_sm # → "0.75rem"
theme.sp(2) # → "8px" (spacing[2])
theme.border_line() # → "1px solid #e0e0e0"
Applying Quasar brand colors:
If the theme has attributes matching Quasar brand color names (primary,
secondary, accent, dark, positive, negative, info,
warning), calling apply() sets them on the app.
This replaces the manual app.set_colors(...) call:
# In your app's __init__:
theme.apply(self) # equivalent to self.set_colors(primary="#164d7d", secondary=...)
Tokens that are not Quasar brand names (muted, surface, border,
font_sm, etc.) are ignored by apply() and are only used in your own
Style definitions.
Helpers:
sp(index)— returnsf"{spacing[index]}px"border_line(width, style, color)— returns a CSS border shorthand, defaults to1px solid {theme.border}
StyleSheet and CssClass — The Efficiency Layer#
Instead of sending full inline CSS strings over the wire for every component,
StyleSheet registers styles as CSS classes and injects a
single <style> tag into the DOM. Components then reference short class
names.
from ngapp.style import Style, StyleSheet
css = StyleSheet()
# Custom sidebar panel — Quasar has no single class for this combination
sidebar = css.add(Style(
height="100%",
overflow_y="auto",
background=theme.surface,
border_right=theme.border_line(),
min_width="200px",
))
# Section heading style used in 3+ places across the app
section_heading = css.add(Style(
font_size=theme.font_sm,
letter_spacing="0.05em",
text_transform="uppercase",
font_weight=700,
color=theme.muted,
))
Composing classes with +:
CssClass objects compose with +. You can combine
them with each other and with Quasar utility classes:
# Custom style + Quasar padding
Div("Properties", ui_class=section_heading + "q-pa-sm")
# Multiple custom styles
Div(ui_class=sidebar + "col-3")
Combining classes with inline overrides:
Use classes for the shared base, inline Style only for the part that
varies per instance:
# Same heading style, different padding per usage site
Div("Properties", ui_class=section_heading, ui_style=Style(padding="12px 16px 8px"))
Div("Settings", ui_class=section_heading, ui_style=Style(padding="8px 12px"))
Injecting into the DOM:
Call inject() once (typically in your app’s __init__):
class MyApp(App):
def __init__(self):
# ... build UI ...
css.inject(self)
This creates a <style> element in the page <head> containing all
registered rules. It uses call_js internally, so it’s safe to call during
__init__.
Scoped Nested Rules — Targeting Child Components#
When you have a container (like a settings panel) and want to automatically
style all Quasar widgets inside it without touching each child component,
use rule() on the class handle returned by
css.add():
panel = css.add(Style(height="100%", overflow_y="auto"))
# Every QCheckbox inside panel gets compact sizing
panel.rule(".q-checkbox__label", font_size="0.82rem")
panel.rule(".q-field--dense .q-field__control", min_height="30px")
panel.rule(".q-slider", margin="4px 0")
This generates CSS like:
.ngs0 .q-checkbox__label { font-size: 0.82rem; }
.ngs0 .q-field--dense .q-field__control { min-height: 30px; }
.ngs0 .q-slider { margin: 4px 0; }
Any component with ui_class=str(panel) automatically applies these rules
to all its descendants — no manual ui_style needed on each widget.
Chaining:
Multiple rules can be chained in a single expression:
panel.rule(".q-checkbox__label", font_size="0.82rem") \
.rule(".q-field--dense", margin_bottom="2px") \
.rule(".q-slider", margin="4px 0") \
.rule(".q-btn--dense", font_size="0.78rem")
Passing Style objects:
You can pass an existing Style object instead of kwargs:
compact_input = Style(min_height="30px", font_size="0.82rem")
panel.rule(".q-field--dense .q-field__control", compact_input)
Dict-style subscript:
An alternative syntax using []:
panel[".q-checkbox__label"] = Style(font_size="0.82rem")
panel[".q-slider"] = Style(margin="4px 0")
Real-world example — property panel styling:
# In styles.py — one block styles the entire property panel
sidebar_props = css.add(Style(height="100%", overflow_y="auto"))
sidebar_props.rule(".q-checkbox", padding="0", min_height="28px") \
.rule(".q-checkbox__label", font_size="0.82rem") \
.rule(".q-field--dense .q-field__control", min_height="32px") \
.rule(".q-field--dense .q-field__label", font_size="0.72rem") \
.rule(".q-slider", margin="4px 0") \
.rule(".q-expansion-item .q-item", padding="8px 12px",
min_height="38px", background="rgba(0,0,0,0.02)")
# Every section added to the panel inherits compact styling automatically
Note
Scoped rules use standard CSS descendant selectors. They work with any
valid CSS selector — class names, pseudo-classes, combinators, etc.
The rules are injected along with all other StyleSheet rules when
css.inject(app) is called.
When to Use What#
Need |
Use |
Example |
|---|---|---|
Padding, margin, flex |
Quasar classes |
|
Show / hide |
|
|
Background / text color from Quasar palette |
Quasar classes |
|
Multi-property style shared across components |
StyleSheet + CssClass |
overlay, sidebar panel, section heading |
Custom positioning, backdrop-filter, gradients, box-shadow combos |
StyleSheet + CssClass |
fixed indicator, visualization toolbar |
Truly dynamic per-instance value |
inline Style |
|
Examples#
Fixed-position status overlay (no Quasar equivalent):
status_bar = css.add(Style(
position="fixed",
bottom="20px",
left="50%",
transform="translateX(-50%)",
background="rgba(15, 23, 42, 0.92)",
backdrop_filter="blur(4px)",
color="white",
padding="8px 20px",
border_radius="8px",
box_shadow="0 4px 12px rgba(0,0,0,0.3)",
z_index=9999,
display="flex",
align_items="center",
gap="8px",
))
indicator = Div("Solving…", ui_class=status_bar)
indicator.ui_hidden = True # use ui_hidden to toggle, not custom CSS
Visualization container with CSS grid (Quasar grid is flex-based, not CSS grid):
viewer_grid = css.add(Style(
display="grid",
grid_template_columns="1fr 280px",
grid_template_rows="auto 1fr auto",
height="100%",
gap="0",
))
viewer_toolbar = css.add(Style(
grid_column="1 / -1",
background="linear-gradient(135deg, #1a1a2e, #16213e)",
color="white",
padding="4px 12px",
display="flex",
align_items="center",
gap="8px",
))
App-specific sidebar with custom border (Quasar drawers exist, but sometimes you need a simpler panel inside a layout):
sidebar_base = css.add(Style(
height="100%",
overflow_y="auto",
background=theme.surface,
))
sidebar_left = css.add(Style(border_right=theme.border_line()))
sidebar_right = css.add(Style(border_left=theme.border_line()))
# Compose: base + side-specific
self.navigator.ui_class = sidebar_base + sidebar_left + "col-3"
self.properties.ui_class = sidebar_base + sidebar_right + "col-3"
Dynamic per-instance color (stays inline — correct for runtime values):
# Each swatch has a different user-chosen color — must be inline
self.ui_style = Style(
background_color=self.to_hex_string(),
border="1px solid rgba(0,0,0,0.15)",
border_radius="4px",
)
API Reference#
See Style for the full class reference.