[Go to site: main page, start]

Skip to content

Build PDFs with Python components

Write functions that return document components. Pass data as arguments, nest children, map over records, and use ordinary Python conditions. Fullbleed serializes the tree and renders it with your styles and explicit engine. You do not need to write HTML strings.

The model is inspired by React's function components and composition. Python evaluates these functions directly to produce static print documents. There is no browser, virtual DOM, hook lifecycle, or JavaScript runtime involved. The complete workflow below uses Fullbleed 2.5.25.

Help shape the next authoring model

The experimental design preview explores components that carry their styles, named customization parts, and tables defined by columns. It includes real PDFs and a pinned source build. That prototype is separate from the published API documented on this page.

The JavaScript component workflow brings named components, attached styles and optional JSX/TSX to Node and browser applications. It does not include Python's semantic validator or promise complete API parity.

Download the complete project Open the component invoice

A component-authored invoice with forest-green display type, data-driven rows, progress bars and a total panel.

A component is a function

from importlib.resources import files
from pathlib import Path
import fullbleed
from fullbleed.ui import (
    Document, Heading, Paragraph, Strong, Stack, StyleSheet, component,
)

@component
def Line(*, label: str, amount: str):
    return Paragraph(Strong(label), ": ", amount)

invoice_styles = StyleSheet({
    "@page": {"size": "A4", "margin": "18mm"},
    "body": {"font_family": "Inter", "color": "#233c36"},
    "h1": {"font_size": "32pt", "letter_spacing": "-1pt"},
    "p": {"margin": 0},
})

@Document(title="Invoice", styles=invoice_styles)
def Invoice(customer: str, items: list[dict]):
    return Stack(
        Heading("Made for ", customer, level=1),
        [Line(label=item["label"], amount=item["amount"]) for item in items],
        gap="12pt",
    )

font = files("fullbleed_assets").joinpath("fonts/Inter-Variable.ttf")
engine = fullbleed.PdfEngine(font_files=[str(font)])
invoice = Invoice("Maple & Finch", [
    {"label": "Design", "amount": "$120.00"},
    {"label": "Review", "amount": "$40.00"},
])
Path("invoice.pdf").write_bytes(invoice.render_pdf(engine))
Path("invoice.png").write_bytes(invoice.render_image_pages(engine, dpi=96)[0])

@component preserves the function and its signature. It is optional for your own components. @Document wraps a component in a document artifact and preserves its callable signature, annotations and docstring. Apply @Document to the outer component; use ordinary functions or @component for nested pieces. Nesting a document artifact raises a clear error.

The designed invoice includes reusable fields, table rows, a total panel, paid/unpaid variants, and styling written in Python.

Props, children and rich text

Props are normal Python arguments. Wrappers accept positional children and forward named props to an underlying component:

from fullbleed.ui import Card, Heading, Paragraph, Strong, component

@component
def Panel(*children, title: str, **props):
    return Card(Heading(title, level=2), *children, **props)

panel = Panel(
    Paragraph("Prepared for ", Strong("Maple & Finch")),
    title="Project summary",
    style={"padding": "18pt", "background": "#f2f6f3"},
)

Text and attribute values are escaped. "<strong>hello</strong>" prints literally; use Strong("hello") for emphasis. Rich text accepts multiple children, including Text("Total: ", Strong("$40")).

Nested lists, tuples and ordered generators become children. Generators are consumed once when constructing an element, so validation and repeated rendering see the same tree. None, False and True omit children; numeric zero remains. Write str(flag) when a boolean should be visible.

from fullbleed.ui import Fragment, Paragraph

body = Fragment(
    [Paragraph(row["description"]) for row in rows],
    paid and Paragraph("Paid in full."),
)

Call component functions explicitly. Functions, mappings, sets and byte buffers are rejected as children instead of printing Python representations. Cycles and content inside void elements also fail clearly.

Named building blocks

Purpose Components
Text Text, Paragraph, Strong, Emphasis, Heading, LineBreak
Structure Fragment, Box, Header, Footer, Section
Layout Stack, Row, LayoutGrid, Inline, Center, Card, Spacer, Divider
Tables Table, TableCaption, THead, TBody, Tr, Th, Td
Lists and media List, ListItem, ListItems, Image, Link
Patterns Badge, SectionHeader, KeyValueRow, PriceRow, IconLabel

Heading(..., level=1) uses a real heading; levels are integers from 1 to 6. Image(src="assets/chart.png", alt="Revenue by quarter") requires an explicit text alternative. Table headers accept scope="col" or scope="row". See semantic authoring for structural validation and accessibility workflows. el(...) remains a lower-level escape hatch; the complete invoice example does not need it.

Class names and styles

class_name labels an element so stylesheet selectors can match it. It does not create or load a style. Put the declarations in a StyleSheet, then attach that sheet to the outer @Document(styles=...).

Argument What it accepts Where it applies
class_name="notice is-paid" Space-separated class names, without dots The element returned by a built-in component
style={"color": "#a64232"} Inline declarations, without a selector That element
@Document(styles=sheet) A stylesheet containing selectors and declarations Matching elements throughout this document

This example prints Paid in full in blue at 18 pt:

from fullbleed.ui import Document, Paragraph, StyleSheet

payment_styles = StyleSheet({
    "@page": {"size": "A5", "margin": "15mm"},
    "body": {"font_family": "Inter"},
    ".notice": {"color": "#245541", "font_size": "18pt", "margin": 0},
    ".notice.is-paid": {"color": "#315d9b"},
})

@Document(title="Payment", styles=payment_styles)
def Payment():
    return Paragraph("Paid in full", class_name="notice is-paid")

With the configured engine from the first example, render with Payment().render_pdf(engine). This passes both the component markup and its attached stylesheet to the engine. Payment().to_html() on its own carries no stylesheet; if you call the engine directly, also pass Payment().to_css().

The dots belong in selectors: .notice matches class_name="notice". .notice.is-paid matches one element with both classes; .notice .notice-title matches a descendant with notice-title inside a notice. Class order in the string does not set precedence. A function named Notice is not itself a CSS selector, and classes are not automatically scoped to a component or Python file.

Forward styling props in your own components

Your function decides which element receives class_name and style. Accept and forward those arguments when callers should be able to customize its root:

from fullbleed.ui import Card

def Notice(*children, class_name="", style=None):
    return Card(
        *children,
        class_name=f"notice {class_name}".strip(),
        style=style,
    )

notice_styles = StyleSheet(payment_styles, {
    ".notice": {"padding": "12pt", "border": "2pt solid #245541"},
    ".notice .notice-title": {"font_size": "14pt", "font_weight": 700},
})

@Document(title="Styled notice", styles=notice_styles)
def StyledNotice():
    return Notice(
        Paragraph("Payment", class_name="notice-title"),
        Paragraph("Paid in full"),
        class_name="is-paid",
        style={"border_color": "#a64232"},
    )

Notice keeps its base notice class and adds the caller's is-paid class. Card also retains its built-in ui-card class. The document combines shared and component rules through StyleSheet(payment_styles, ...); defining a sheet beside a function does not attach it automatically. When splitting files, import both the component and its sheet, and compose the sheets on the document.

The title's descendant rule sets 14 pt bold text. The inline border_color changes this one notice's border to rust red. Ordinary stylesheet rules follow selector specificity, then source order for ties. Inline declarations override ordinary stylesheet declarations on the same element; !important changes that priority. Text properties such as color can inherit into children, while padding and borders apply to the element itself. Layout props such as Stack(gap="12pt") produce inline styles too; use an explicit style prop to override those values.

Use component-specific class names when combining designs to avoid unintended matches. Fullbleed does not generate CSS Modules names or import a framework's stylesheet from a class string. See the layout and composition options below.

Layout and style composition

Stack(..., gap="12pt"), Row(..., justify="space-between", wrap=True), and LayoutGrid(..., columns=3, gap="10pt") turn layout props into CSS. Grid columns can also be a track string such as "2fr 1fr". These props establish the display mode. A supplied style= overrides their declarations.

Element styles accept mappings, CSS strings, or composed fragments:

from fullbleed.ui import Paragraph, style, pt

base = style(font_size=pt(10), color="#233c36")
important = Paragraph("Due today", style=style(base, font_weight=700))

style(base, ...) leaves base unchanged. Repeated properties use the last authored value and move to the end. Snake-case names become kebab-case; CSS custom properties keep their case. Units are explicit: write "12pt" or pt(12). Quoted strings, comments, function arguments and data URLs retain their declaration boundaries during style composition.

StyleSheet maps selectors to style values. Compose shared sheets and overrides with StyleSheet(shared, overrides); rule order uses the normal CSS cascade. Mappings support declaration rules such as @page. Use a CSS string for nested at-rules.

Artifact rendering includes small layout defaults for rows, stacks, grids, inline groups, centers, cards and tables. It loads no theme or fonts. Your rules follow the defaults. Set include_component_styles=False on @Document for complete control, or call component_styles() in a separate rendering pipeline.

Rendering and exporting

Construct PdfEngine explicitly to choose fonts, assets, profiles and diagnostics. PDF and PNG methods use the same component and style path:

pdf = invoice.render_pdf(engine, a11y_mode="raise")
pages = invoice.render_image_pages(engine, dpi=96)
invoice.emit_artifacts(
    html_path="output/invoice.html",
    css_path="output/invoice.css",
)

The HTML export links its emitted stylesheet for browser inspection. to_html() alone serializes HTML; it does not embed styles or make the engine fetch CSS URLs. to_css() returns composed CSS separately. Existing engine.render_pdf(artifact.to_html(), css) calls retain their explicit styling.

Set actual paper geometry with @page or engine options. The decorator's existing page and margin fields remain authoring metadata. HTML title/language and engine-level PDF metadata are separate; configure the latter on the engine when a profile requires it.

a11y_mode="raise" checks structural semantics, not PDF/UA conformance. Review final output and use the relevant delivery verifier. Component names, selectors, CSS and URL destinations are developer-authored inputs. Escaping data does not turn this API into an HTML sanitizer.

Compatibility and other languages

Existing helpers, decorators and scaffold styles continue to work. Changes in 2.5.25 intentionally stop printing collection/generator representations and boolean placeholders. HTML boolean attributes retain presence/absence behavior; ARIA and data booleans emit "true" or "false". Invalid names, cycles and children on void elements raise errors. Format these inputs explicitly if they were previously intentional text.

Python is the reference authoring experience. JavaScript/TypeScript components are published in npm package 0.6.0, with ordinary functions and optional JSX/TSX. C# components are available in FullBleed.DotNet 0.2.0, with typed methods, LINQ children and immutable styles. Rust accepts HTML/CSS. The JavaScript and C# layers do not include Python's semantic validator or promise complete API parity.