Skip to content

Style Model

Styling in ODT is structural. A visual result may involve text properties, paragraph properties, table-cell properties, graphic styles, and style definitions stored in different parts of the ODT package.

The engine therefore does not treat every style option as one undifferentiated CSS-like array.

The most important rule is to style the correct document layer.

Style responsibilities

Think of generated content in layers:

Document structure
│
├── Paragraph
│   ├── paragraph style
│   │   ├── margins
│   │   ├── alignment
│   │   ├── indentation
│   │   ├── borders
│   │   ├── padding
│   │   └── line height
│   │
│   └── text runs
│       ├── font family
│       ├── font size
│       ├── bold / italic
│       ├── underline
│       └── color
│
├── Table cell
│   ├── background
│   ├── border
│   └── padding
│
└── Image / frame
    ├── size
    ├── anchor
    ├── wrap
    └── positioning

This distinction matters because ODF serializes these responsibilities differently.

Text styles

Text styles describe inline text runs:

$paragraph->addText('Important', [
    'bold' => true,
    'italic' => true,
    'color' => '#12324a',
    'font-size' => '11pt',
    'font-family' => 'Arial',
]);

The engine maps these friendly options to ODF text properties and creates the required style definitions.

Paragraph styles

Paragraph properties describe the block rather than its characters:

$paragraph = new Paragraph(null, [
    'margin-top' => '0.3cm',
    'margin-bottom' => '0.1cm',
    'text-align' => 'left',
    'border-bottom' => '1pt solid #12324a',
    'padding-bottom' => '0.05cm',
]);

Do not encode paragraph spacing by adding empty text or repeated line breaks when a paragraph margin expresses the intention more accurately.

Table-cell styles

A table cell has another style layer:

$cell = new RichTableCell('Total', [
    'background' => '#eeeeee',
    'border' => '0.5pt solid #999999',
    'padding' => '0.15cm',
    'text-align' => 'right',
    'bold' => true,
]);

The convenience API separates cell, paragraph, and text options before rendering. This is useful for small examples, but understanding the layers remains important when debugging a complex document.

Automatic styles and named styles

For local styling, the engine can generate style names automatically from style definitions.

For complex documents, semantic named paragraph styles can make the generated ODT easier to understand and maintain:

$template->styles()->defineParagraph('CVEntryTitle', [
    'margin-top' => '0.1cm',
    'margin-bottom' => '0.03cm',
]);

$paragraph = new Paragraph('CVEntryTitle');

Names such as CVEntryTitle, ReportHeading, or InvoiceTotal communicate intent when inspecting the generated XML or editing the resulting document.

A named reference does not create a definition. new Paragraph('CVEntryTitle') means "use this paragraph style in the current document". Define generated reusable paragraph styles explicitly through $template->styles()->defineParagraph(...), or reference a style already authored in the ODT template.

Public authoring model

The current public styling model has three layers:

Application authoring
    element options / fluent element APIs

Document style authoring
    $template->styles()->defineParagraph(...)

Custom structured-element extension
    getOwnStyleRequirements()
    ownedElements()
    typed resource/dependency hooks
    toDomNode()

For ordinary application code, the first two layers are normally sufficient.

DocumentStyles is the public document-oriented facade for generated named paragraph definitions. It does not own process-global state; definitions belong to the current logical document.

Custom OdtElement implementations can describe semantic style requirements through StyleRequirement without registering styles in a global registry.

Internal style pipeline

The implementation behind the public API is document-local:

Element options / DocumentStyles / custom StyleRequirement
                    ↓
             StyleRequirement
                    ↓
              StyleContext
                    ↓
               materializers
                    ↓
             ODF style output

StyleContext is an internal document-local semantic authority. StyleMapper is a stateless mapping and identity utility. StyleWriter is a narrow serialization helper for explicit data and is not an application-facing style registry or authoring API.

There is no process-global paragraph/text registry fallback in the current style architecture.

Where styles are stored

An ODT package can contain styles in more than one XML location.

The engine writes document styles primarily through styles.xml, while generated automatic structures such as table-column and table-cell styles may also involve content.xml automatic styles.

You normally do not need to manage those XML locations manually. The distinction becomes important when diagnosing missing or duplicated styles.

Prefer semantic intent over visual hacks

When possible:

  • use paragraph margins instead of empty paragraphs for spacing;
  • use ListElement instead of a bullet character;
  • use RichTable instead of aligned spaces for tabular data;
  • use table-cell padding instead of spaces inside a cell;
  • use paragraph styles for paragraph geometry and text styles for characters.

This produces more native, editable ODF and usually behaves better when the document is modified in LibreOffice.

Verification

For style-sensitive features, the project's effective verification model is:

PHPUnit / regression tests
        +
ODT package and XML verification
        +
LibreOffice visual inspection

An ODT package can be structurally valid while still producing an unexpected office-layout result. Conversely, a visually plausible document can hide incorrect style ownership in the XML. Both levels matter for complex document generation.

Continue with Text & Paragraph Styles and Table & Cell Styles for practical examples.