Skip to content

Native Backgrounds And Gradients

Status

Two schema-validated implementation slices are in place:

  1. Linear gradient fills on slide backgrounds and shape fills (<a:lin>).
  2. Radial gradient fills on the same surfaces (<a:path path="circle"> with a <a:fillToRect> focus). See Radial gradients below.

The configured ooxml and microsoft_learn MCP servers were consulted on 2026-06-07 (linear) and the radial shade elements re-checked against the ooxml MCP on 2026-06-15. The OOXML shapes below are schema-grounded, but PowerPoint-authored comparison fixtures are still required before broadening the API or treating native gradients as a drop-in replacement for deterministic raster gradients.

Goal

Add reliable native PPTX gradient fill support for reusable ts-pptx surfaces, starting with simple linear gradients on slide backgrounds and shape fills.

This belongs in ts-pptx when the behavior is generic, schema-valid, and stable across PowerPoint-compatible consumers. A consumer should continue using deterministic raster backgrounds when exact rendered appearance is more reliable than native PPTX gradients.

MCP Review Findings

Sources consulted:

Confirmed XML constraints for the first slice:

  1. p:bgPr uses CT_BackgroundProperties: exactly one DrawingML fill choice first, then optional DrawingML effects, then optional p:extLst. Keep emitting <a:effectLst/> after the fill to match the current regression guard and schema order.
  2. Legal p:bgPr fill choices are a:noFill, a:solidFill, a:gradFill, a:blipFill, a:pattFill, and a:grpFill. Image backgrounds should keep using a:blipFill; this plan only adds a:gradFill.
  3. Presentation shape p:spPr uses the same DrawingML fill group after geometry and before a:ln. The existing shape fill insertion point is the right one.
  4. a:gradFill serializes children in this order: optional a:gsLst, optional shade properties (a:lin or a:path), then optional a:tileRect. The first implementation should emit a:gsLst followed by a:lin and should omit a:path and a:tileRect.
  5. a:gradFill supports optional rotWithShape and flip attributes. flip values are none, x, y, and xy, but it is only relevant once a:tileRect is supported, so leave it out of the public API initially.
  6. a:gsLst contains two or more a:gs children. Each a:gs requires a pos attribute and exactly one DrawingML color choice child.
  7. a:gs/@pos is an ST_PositiveFixedPercentage; public stop positions should be finite 0..100 percentages and serialize as 0..100000.
  8. a:lin/@ang is an ST_PositiveFixedAngle in 60000ths of a degree with legal values from 0 through less than 21600000. Public angles must be normalized into 0 <= angle < 360 before serialization. Do not allow 360 to serialize as 21600000.
  9. a:lin/@scaled is an optional boolean. Emit it only when the public API specifies it, or choose a documented default after comparing PowerPoint fixtures.
  10. p:bgPr/@shadeToTitle and the PowerPoint API ShadeFromTitle gradient type are separate from simple linear gradients. Leave shade-to-title support out of the first slice.

Microsoft Learn confirms that PowerPoint exposes background fill types Solid, Gradient, PictureOrTexture, and Pattern, and gradient subtypes Linear, Radial, Rectangular, Path, and ShadeFromTitle. The JavaScript API only exposes a high-level gradient type option for slide backgrounds, so it is useful compatibility signal but not a substitute for inspecting PowerPoint-authored package XML.

PowerPoint Fixture Review Still Outstanding

Before broadening support beyond the first linear-gradient slice, complete the behavior review that MCP lookup cannot replace:

  1. Compare at least one minimal PowerPoint-authored PPTX fixture for a slide background gradient and one for a shape gradient.
  2. Confirm PowerPoint's authored defaults for a:gradFill/@rotWithShape, a:lin/@scaled, gradient stop ordering, and whether it writes two stops or inserts midpoint stops for common presets.
  3. Record only small repo-specific findings and section references. Do not vendor full standards text or large extracted reference material.

Do not expand the public guarantee until the PowerPoint fixture review confirms PowerPoint's authored XML shape and rendering behavior.

Proposed API Slice

Start with a narrow ShapeFillProps extension for linear gradients:

ts
fill: {
	type: 'gradient',
	gradient: {
		kind: 'linear',
		angle: 90,
		scaled: true,
		rotateWithShape: true,
		stops: [
			{ position: 0, color: '451DC7' },
			{ position: 100, color: '0B003D', transparency: 10 },
		],
	},
}

The first slice avoided path, radial, tile-rectangle, table, chart, and line-gradient promises until each parent surface is independently validated. Radial fills were subsequently added on the validated background/shape surfaces (see Radial gradients); tile-rectangle, table, chart, and line gradients are still out of scope.

The same model should apply directly to slide backgrounds because BackgroundProps already extends ShapeFillProps:

ts
slide.background = {
	type: 'gradient',
	gradient: {
		kind: 'linear',
		angle: 90,
		stops: [
			{ position: 0, color: '451DC7' },
			{ position: 100, color: '0B003D' },
		],
	},
}

Initial validation rules:

  1. Require type: 'gradient' plus gradient.kind: 'linear'.
  2. Require at least two stops.
  3. Require finite stop positions in 0..100; reject or warn on invalid input according to the local fill API style chosen during implementation.
  4. Prefer nondecreasing stop positions for deterministic output. If the helper sorts stops, document that behavior in the public API docs and tests.
  5. Require finite angles, normalize to 0 <= angle < 360, and serialize only legal ST_PositiveFixedAngle values.
  6. Support HexColor, ThemeColor, and existing alpha/transparency handling for each stop by routing stop colors through createColorElement().

Radial gradients

Radial fills reuse the linear plumbing (same type: 'gradient' fill object, same <a:gradFill>/<a:gsLst> stop list and color handling) and swap the shade element from <a:lin> (linear) to <a:path path="circle"> (radial).

ts
fill: {
	type: 'gradient',
	gradient: {
		kind: 'radial',
		// Focus of the bright center as a percentage of the fill box.
		// Omitted => { x: 50, y: 50 } (dead center).
		center: { x: 50, y: 50 },
		rotateWithShape: true,
		stops: [
			{ position: 0, color: 'FFFFFF' }, // position 0 is the center color
			{ position: 100, color: '0B003D' }, // later stops fan out to the edges
		],
	},
}

The same object works on slide.background because BackgroundProps extends ShapeFillProps.

Serialization

A radial gradient serializes as a <a:gradFill> (§20.1.8.33) whose shade child is <a:path path="circle"> (§20.1.8.46; ST_PathShadeType shape | circle | rect, §20.1.10.38) carrying a single <a:fillToRect> focus rectangle (§20.1.8.31):

xml
<a:gradFill rotWithShape="1">
	<a:gsLst>
		<a:gs pos="0"><a:srgbClr val="FFFFFF"/></a:gs>
		<a:gs pos="100000"><a:srgbClr val="0B003D"/></a:gs>
	</a:gsLst>
	<a:path path="circle">
		<a:fillToRect l="50000" t="50000" r="50000" b="50000"/>
	</a:path>
</a:gradFill>

Notes that make the mapping correct:

  1. Stop direction is inverted from the geometric reading. For <a:path>, the stop at pos="0" is the center and later stops radiate outward: the opposite mental model from a linear sweep. The public position ordering is unchanged (0 first), so position: 0 is documented as the center color.
  2. fillToRect is a focus rectangle, not a center point. Its l/t/r/b are ST_Percentage insets from each edge of the tile (§20.1.10.40; positive = inset, negative = outset), in 1000ths of a percent (50%50000). Equal 50000 insets on all sides collapse the focus to the dead-center point, which is the default center: { x: 50, y: 50 }.
  3. center maps to insets, not coordinates. center.x/center.y are clamped to 0..100 and mapped as l = x, t = y, r = 100 - x, b = 100 - y (each × 1000). So center: { x: 25, y: 50 } emits l="25000" t="50000" r="75000" b="50000", pushing the bright center to 25% from the left edge.
  4. rotWithShape defaults to 1 exactly as for linear. scaled is a linear-only attribute (<a:lin/@scaled>) and is rejected/ignored on radial.

Validation

In addition to the shared stop rules (≥2 stops, finite 0..100 positions, ascending serialization order), radial adds:

  1. gradient.kind: 'radial' selects the radial path.
  2. center.x and center.y are clamped into 0..100; an absent center defaults to { x: 50, y: 50 }.
  3. There is no angle on a radial gradient (it has no linear direction).

Still outstanding for radial

PowerPoint authors radial fills with additional preset shade variants (path="shape", path="rect", and off-center fillToRect presets such as the corner "from corner" gradients). Only path="circle" with an axis-aligned fillToRect focus is currently exposed. Confirm PowerPoint's authored defaults for those presets against minimal fixtures before extending the public API.

Implementation Steps

  1. Extend ShapeFillProps in src/types/index.ts with a linear gradient model and exported stop type. Expand type from 'none' | 'solid' to include 'gradient'.
  2. Add a genXmlGradientFill() helper in src/gen/drawingml/fill.ts, called by genXmlColorSelection() when type: 'gradient'. Keep genXmlColorSelection returning a complete fill-choice element.
  3. Reuse createColorElement() for stop colors so hex, theme color, and alpha behavior stays consistent with existing solid fills.
  4. Convert stop positions from public 0..100 values to OOXML 0..100000 values with Math.round(position * 1000).
  5. Normalize gradient angles before calling convertRotationDegrees(), or harden convertRotationDegrees() so 360, negative values, and values above one turn cannot produce invalid ST_PositiveFixedAngle values.
  6. Serialize the first slice as:
xml
<a:gradFill rotWithShape="1">
	<a:gsLst>
		<a:gs pos="0"><a:srgbClr val="451DC7"/></a:gs>
		<a:gs pos="100000"><a:srgbClr val="0B003D"/></a:gs>
	</a:gsLst>
	<a:lin ang="5400000" scaled="1"/>
</a:gradFill>
  1. Update slide background emission in src/gen/slide/ so native background fills are emitted when slide.background contains either a solid color or a gradient, while _bkgdImgRid image backgrounds keep taking precedence. The current slide.background?.color condition is too narrow for gradient backgrounds.
  2. Keep shape fill support on the existing genXmlColorSelection() path so p:spPr still emits geometry, then fill, then a:ln.
  3. Avoid using deprecated background.fill as the gradient API. It currently aliases a legacy color string to background.color; direct background: { type: 'gradient', gradient: ... } is clearer and matches BackgroundProps extends ShapeFillProps.
  4. Leave chart fills, table fills, and line fills out of the initial public guarantee unless the full review proves their current call sites can support gradient-only fill objects safely.

Fixtures And Tests

Add executable evidence with a small blast radius:

  1. Add regression tests that inspect ppt/slides/slide1.xml for a:gradFill under a shape p:spPr and under slide background p:bgPr.
  2. Add schema fixtures in test/schema-cases.js for a native shape gradient and a native slide background gradient.
  3. Assert that slide background gradients keep <a:effectLst/> after a:gradFill inside p:bgPr.
  4. Assert that shape gradients appear in the fill slot before a:ln inside p:spPr.
  5. Add invalid-input coverage for fewer than two stops, out-of-range stop positions, and angle normalization around 0, 359, 360, and negative values.
  6. Add or document minimal PowerPoint-authored comparison fixtures for the same two cases.
  7. Run:
bash
pnpm run build
pnpm run typecheck
pnpm run test:unit
pnpm run test:schema

If test:schema cannot run because the validator is missing, install it with ./tools/ooxml-validator/install.sh before treating the implementation as accepted.

Downstream Fallback Guidance

A consumer should not remove its raster gradient guidance immediately.

Native gradients can replace deterministic raster backgrounds only after:

  1. The generated PPTX opens without repair in PowerPoint.
  2. The gradient survives import or rendering through the target review tools.
  3. The rendered result is close enough for the specific deck workflow.
  4. A single native background object remains easier to preserve semantically than an image object.

Until then, a consumer should continue preferring one deterministic image object over many adjacent solid-color rectangles for continuous gradient backgrounds.