← Back to blog

Gradients, shadows, and page backgrounds in Forme 0.10.0

Six new visual style properties land in Forme 0.10.0 — CSS gradients (linear and radial, multi-stop), boxShadow, opacity that actually cascades, rounded clipping, and Page backgrounds for watermark overlays. All in JSX, all rendered by the Rust engine.

PDF generators usually treat visual styling as an afterthought. You can put text on a page, you can color a rectangle — but the moment you want a gradient header, a soft drop shadow on an invoice total, or a subtle watermark behind every page, you hit a wall. Either the library doesn't support it, or it supports it through a side-door API that bypasses the layout system entirely.

Forme 0.10.0 lands six visual style properties that work the same way CSS works in a browser. You put them in your style prop and they render — no special components, no escape hatches, no manual coordinate math.


What shipped

  1. background accepts CSS gradient strings — linear and radial, with multiple color stops.
  2. boxShadow for offset drop shadows that honor borderRadius.
  3. opacity now cascades to children, the way CSS opacity works.
  4. overflow: 'hidden' + borderRadius clips to the rounded corners (not a sharp rectangle).
  5. <Page backgroundImage> with backgroundSize, backgroundPosition, and backgroundOpacity — built-in watermark support.
  6. wordSpacing as a first-class style property.

Each one solves a problem that previously required either accepting a limitation or hand-positioning rectangles. Let's go through them.


CSS gradients in background

You can write CSS gradient strings exactly the way you'd write them in a browser. Forme parses them and the engine renders them as native PDF Shading dictionaries — not as a raster image baked into the file. That means they stay crisp at any zoom and add only a few hundred bytes to the output.

<View style={{
  background: 'linear-gradient(135deg, #667eea, #764ba2)',
  padding: 24,
  borderRadius: 12,
}}>
  <Text style={{ color: '#fff', fontSize: 18, fontWeight: 700 }}>
    Premium plan
  </Text>
</View>

Radial gradients work the same way:

<View style={{
  background: 'radial-gradient(circle, #10b981, #059669)',
  padding: 20,
  borderRadius: 100,
}}>
  ...
</View>

Multi-stop gradients are supported via the engine's Type 3 stitching function:

<View style={{
  background: 'linear-gradient(90deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)',
  height: 8,
  borderRadius: 4,
}} />

CSS angle conventions hold: 0deg is bottom-to-top, 90deg is left-to-right, 180deg is top-to-bottom. Side keywords (to right, to bottom right, etc.) and turn / rad / grad units all parse.


boxShadow

A drop shadow behind any View, with the offset and color you'd expect:

<View style={{
  backgroundColor: '#fff',
  borderRadius: 12,
  padding: 20,
  boxShadow: { offsetX: 0, offsetY: 4, blur: 0, color: '#00000020' },
}}>
  <Text>Invoice total: $4,800.00</Text>
</View>

You can pass the object form or a CSS-like string:

<View style={{ boxShadow: '2 4 0 #00000033' }}>...</View>

The shadow honors borderRadius — rounded boxes get rounded shadows. blur is parsed and accepted but rendered as a sharp shadow in this release; soft blur is queued for a later release because it requires a different rendering path (PDF Soft Masks).


Opacity that cascades

In CSS, setting opacity: 0.5 on a <div> fades the entire subtree — background, borders, text, child elements, all of it. Forme 0.9.x set opacity only on the View's own paint, leaving any text inside at full alpha. That was a bug.

Forme 0.10.0 wraps the entire element subtree in the PDF graphics-state save/restore, so opacity behaves like CSS:

<View style={{ opacity: 0.6, backgroundColor: '#e0e7ff', padding: 20 }}>
  <Text>This text now fades along with the background.</Text>
  <View style={{ opacity: 0.5, padding: 12 }}>
    <Text>Nested opacities multiply — this is at 0.6 × 0.5 = 0.3.</Text>
  </View>
</View>

Nested opacities multiply naturally via the PDF graphics-state stack — no math, no manual color manipulation.


Rounded clipping when overflow: hidden

Setting overflow: 'hidden' on a View with borderRadius used to clip children to a sharp rectangle, then the rounded background-color paint would sit on top. The effect: images and overflowing content visibly escaped the rounded corners.

0.10.0 clips to the rounded path itself:

<View style={{
  overflow: 'hidden',
  borderRadius: 16,
  width: 240,
  height: 160,
}}>
  <Image src="data:image/jpeg;base64,..." />
</View>

The image is now clipped to the rounded corners. This is what you'd expect from a <div> with overflow: hidden and border-radius in CSS — it now works the same way in PDF.


Page backgrounds (watermarks, finally)

Put a background image behind every page's content:

<Document>
  <Page
    size="Letter"
    margin={54}
    backgroundImage="https://cdn.example.com/draft-watermark.png"
    backgroundSize="cover"
    backgroundPosition="center"
    backgroundOpacity={0.08}
  >
    <Text>Confidential — Draft</Text>
    {/* ...rest of the page... */}
  </Page>
</Document>

backgroundSize accepts 'fill' (stretches to the page bounds, default), 'cover', or 'contain'. backgroundPosition accepts 'center' and the four corner keywords. backgroundOpacity makes watermark overlays trivial.

The engine deduplicates: the same URL across multiple pages embeds the image once and references it via a single PDF XObject. A 300-page contract with a logo on every page costs the same as a 1-page contract with a logo.


wordSpacing

A long-requested style prop that maps to the PDF Tw operator. Useful for setting tighter or looser word spacing without changing font metrics:

<Text style={{ wordSpacing: 4, fontSize: 14 }}>
  Extra space between words for readability.
</Text>

It stacks additively with text-align: 'justify' — if you've set a positive wordSpacing and also use justified alignment, your value becomes the floor and justification adds slack on top.


A realistic example

Putting several of these together, here's an invoice header that would have required a lot of workarounds in 0.9:

<View style={{
  background: 'linear-gradient(135deg, #1e293b, #334155)',
  padding: 32,
  borderRadius: 16,
  boxShadow: { offsetX: 0, offsetY: 8, blur: 0, color: '#00000022' },
  marginBottom: 24,
}}>
  <Text style={{
    color: '#fff',
    fontSize: 28,
    fontWeight: 700,
    letterSpacing: -0.5,
  }}>
    Invoice #{invoice.number}
  </Text>
  <Text style={{
    color: '#cbd5e1',
    fontSize: 12,
    wordSpacing: 1,
    marginTop: 8,
  }}>
    Due {formatDate(invoice.dueDate)} · ${invoice.total.toFixed(2)}
  </Text>
</View>

Dark gradient header, soft drop shadow, properly rounded corners, tight letter spacing, comfortable word spacing. All in the style prop. The engine renders it as native PDF — no rasterization, no embedded image, no manual coordinate calculation.


How it's rendered

These aren't visual hacks. The engine generates:

  • Gradients as PDF Shading dictionaries — Type 2 (axial / exponential) for two-stop gradients, Type 3 (stitching) for multi-stop. Same primitives Adobe Illustrator and InDesign export.
  • Shadows as filled rectangles (or rounded paths) painted behind the element, with alpha routed through the existing ExtGState system.
  • Opacity as graphics-state save/restore (q ... Q) wrapping the entire element subtree. Nested opacities multiply via the stack.
  • Rounded clipping as a path-based clip (m l c h W n) instead of re W n.
  • Page backgrounds as PDF XObjects deduplicated by URL across pages.

The output is a real PDF with real PDF primitives. It opens identically in Chrome, Acrobat, Preview, Foxit, and any PDF/A or PDF/UA validator that supports the relevant features.


Getting started

npm install @formepdf/react@0.10.0 @formepdf/core@0.10.0

If you use the hosted API, no client update is needed — formepdf/forme:0.10.0 rolled out automatically and renders every new template with the visual props above. Existing templates render identically; the changes are additive.

For local development, the VS Code extension's live preview also picks up 0.10.0's WASM bundle — install or upgrade from the marketplace to see gradients and shadows render inline as you write JSX.

import { renderDocument } from '@formepdf/core';
import { Document, Page, View, Text } from '@formepdf/react';

const pdf = await renderDocument(
  <Document>
    <Page size="A4" margin={54}>
      <View style={{
        background: 'radial-gradient(circle, #10b981, #059669)',
        padding: 24,
        borderRadius: 12,
        boxShadow: { offsetX: 0, offsetY: 4, blur: 0, color: '#00000030' },
      }}>
        <Text style={{ color: '#fff', fontSize: 18 }}>
          Hello, gradients
        </Text>
      </View>
    </Page>
  </Document>
);

Full changelog at the Forme GitHub release page. Try the new properties live at playground.formepdf.com, or sign up for a free API key at app.formepdf.com — the free plan includes 50 renders per month.