Styling with CSS¶
pgwidgets uses standard CSS classes for all widget styling. You can customize the appearance of widgets by overriding these classes in your own stylesheet.
Widget CSS Classes¶
Each widget type has its own CSS class name:
Widget |
CSS Class |
|---|---|
Label |
|
Button |
|
TextEntry |
|
TextArea |
|
VBox |
|
HBox |
|
Splitter |
|
TabWidget |
|
CheckBox |
|
Slider |
|
ProgressBar |
|
TreeView |
|
The full set of classes can be found in the individual CSS files under
css/.
Customizing Defaults¶
Override the CSS classes to change the default appearance of all instances of a widget type:
/* Round all buttons and change the default color */
.button-widget {
border-radius: 8px;
background-color: #336699;
color: white;
}
/* Add a subtle shadow to all labels */
.label-widget {
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
}
/* Custom scrollbar thumb color */
.scrollbar-thumb {
background-color: #888;
border-radius: 4px;
}
You can also target specific widgets by adding your own CSS class or ID to the widget’s element:
let btn = new Widgets.Button("Special");
btn.get_element().classList.add('my-special-button');
.my-special-button {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
Programmatic Style Overrides¶
pgwidgets provides methods like set_color(), set_font(),
set_border_width(), resize(), and set_padding() that set
inline styles on the widget’s DOM element. Inline styles have the
highest CSS specificity, so they override any CSS class rules.
This means:
CSS classes control the default appearance.
Programmatic methods override per-instance when called.
For example:
/* This sets the default label color */
.label-widget {
color: navy;
}
let a = new Widgets.Label("Uses CSS default"); // navy text
let b = new Widgets.Label("Overridden");
b.set_color(null, "red"); // inline style wins — red text
Properties not set by any programmatic method are always safe to
style via CSS. These include border-radius, box-shadow,
transition, opacity, text-transform, letter-spacing,
cursor, and many others.
The following properties can be set by programmatic methods and will override CSS when called:
Method |
CSS Properties Affected |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If you need to override an inline style from CSS, !important will
work but should be used sparingly:
/* Force all labels to be navy, even after set_color() */
.label-widget {
color: navy !important;
}
A better approach is to avoid mixing CSS and programmatic styles for the same property – use CSS for global theming and programmatic methods for dynamic per-widget changes.
Custom Fonts¶
Two new application-level methods (added since v0.3.0) let the
Python side push custom font files to the browser and apply a
document-wide default font. The mechanism is the
FontFace API + document.fonts.add() plus a small
managed <style> element that writes CSS variables on
:root.
Registering a font¶
app.register_font('Roboto', '/path/to/Roboto-Regular.ttf')
app.register_font('Roboto', '/path/to/Roboto-Bold.ttf',
weight='bold')
app.register_font('Roboto', '/path/to/Roboto-Italic.ttf',
style='italic')
familyis the CSSfont-familyname you’ll reference from widgetset_font(...)calls or from CSS. Names are passed through verbatim – pick any case convention and stay consistent.sourceis a path (stroros.PathLike) or rawbytes(e.g.font_path.read_bytes()or an in-memory font from another library).weightaccepts CSS keywords ('normal','bold','bolder','lighter') or numeric strings ('100'through'1000'). The JS handler also translates common TTF metadata names likethin,light,medium,semibold,extrabold,black,heavy, etc. to their numeric equivalents per the CSS Fonts spec, so passing weights straight from a font registry usually Just Works.styleaccepts'normal','italic', or'oblique'(synonyms like'roman'/'slanted'get normalised too).
Multiple registrations against the same family with
different weight / style combine into a single CSS
font-family with multiple faces – exactly the same way an
@font-face author would express it.
The bytes are served via the built-in HTTP server at
/_pgwidgets/font/<id> with a long Cache-Control:
immutable header. When pgwidgets is embedded behind a Flask
/ nginx stack, the route is served from the same Python process
so it passes through naturally; under pyodide the URL is
relative to window.location.href.
Default font¶
app.set_default_font('Roboto', size=13)
Writes --pg-default-font-family,
--pg-default-font-size, --pg-default-font-weight and
--pg-default-font-style CSS variables on :root (via a
managed <style id="pg-default-font"> element). The base
widget stylesheet consumes them on body:
body {
font-family: var(--pg-default-font-family, sans-serif);
font-size: var(--pg-default-font-size, 13px);
font-weight: var(--pg-default-font-weight, normal);
font-style: var(--pg-default-font-style, normal);
}
Anything that doesn’t set a font of its own inherits the
default. Per-widget set_font(...) still wins via
inline-style specificity, and the fallback values in the
var(...) calls are what you get when set_default_font
is never called – so existing apps see no behavioural change.
Pass family=None to clear the default and fall back to the
stylesheet values.
Reconstruction¶
Font registrations are replayed to every new and reconnecting
session before on_connect / reconstruct runs, so a
widget reconstructed with set_font(family, ...) always
finds the face already declared.
Caveats¶
Browsers may render fallback text in the brief window between the
register-fontmessage arriving andface.load()resolving (standard FOUT behaviour). For most apps this flash is invisible; if it matters you canawait document.fonts.readyin your bootstrap.The bytes are kept in memory on the Python side for the lifetime of the
Application. A 200 KiB TTF is fine; a 4 MiB color-emoji set might warrant chunking or on-disk caching.
Handling CSS Conflicts with Other Libraries¶
pgwidgets CSS classes are specific (e.g. .splitter-widget,
.vbox-container), so direct class name collisions with other libraries
are unlikely.
The more common issue is broad CSS resets from other frameworks
(Bootstrap, Tailwind, Normalize.css, etc.) that apply rules to bare
elements like div, input, button, or *. These can
affect pgwidgets elements in subtle ways – unexpected margins, different
box-sizing, altered font sizes.
Strategies for handling conflicts:
Load order – load pgwidgets CSS after the framework’s CSS so that pgwidgets rules take precedence for equal specificity.
CSS layers – modern browsers support
@layerto control cascade priority:@layer framework, pgwidgets; @layer framework { @import "bootstrap.min.css"; } @layer pgwidgets { @import "Widgets.css"; }
Rules in the
pgwidgetslayer will always beat theframeworklayer regardless of specificity.Shadow DOM – for full isolation, mount pgwidgets inside a shadow root. No styles leak in or out. See Integrating with Web Frameworks for an example.
Scoped container – reset inherited styles at the pgwidgets mount point:
.pgwidgets-root { all: initial; font-family: sans-serif; font-size: 14px; }
This prevents the host page’s styles from cascading into the pgwidgets subtree.
In practice, most integrations work without any special handling. Try it first and address specific conflicts as they arise.