Skip to content

Theming

Every colour, corner radius and font on the generated pages uses a --pyd-* custom property. To retint the pages, set a handful of these properties. Anything the tokens do not cover is a normal CSS rule, and your rules win by default. See Overriding rules.

To replace a whole renderer, for example to build signature blocks or member tables with your own Astro component, see Component overrides instead.

The plugin’s stylesheet is starlight-pydocs/styles. Under Starlight, the plugin appends it to Starlight’s customCss for you. Pass injectStyles: false if you want to import it yourself.

Add your own CSS to that same list. Register it like any other Starlight custom stylesheet:

astro.config.mjs
starlight({
title: 'My project',
plugins: [starlightPydocs({ packages: [{ name: 'mypkg', search: ['../src'] }] })],
customCss: ['./src/styles/custom.css'],
});

Both kinds of change go in that file. To retint, set tokens. To override, write a rule:

src/styles/custom.css
:root {
--pyd-accent: #b45309;
--pyd-radius: 0;
}
/* Unlayered, so this rule beats the plugin
stylesheet with no !important. */
.pyd-signature {
background: transparent;
border-inline-start: 3px solid var(--pyd-accent);
}

In a plain Astro project nothing is injected. The plugin’s built-in page layout imports the stylesheet itself, so the generated pages are styled either way. Import it yourself when you pass a layout of your own as layout, or in a hand-written page that embeds API docs with <Autodoc>:

src/layouts/MyLayout.astro
---
import 'starlight-pydocs/styles';
---

Every rule in the stylesheet is wrapped in @layer starlight-pydocs. An unlayered rule always beats a layered rule, whatever its specificity. So a plain selector in your own stylesheet wins, with no specificity fight and no !important. You do not need to increase selector specificity to override the plugin’s styles.

Keep your overrides unlayered, and this holds however you load them. If you put your own rules in a cascade layer, the comparison changes. The plugin appends its stylesheet after your customCss entries, so its layer is declared last, and later-declared layers win.

One exception: inside Starlight’s own .sl-markdown-content, Starlight’s unlayered rules apply too. There, give your rule enough specificity to beat Starlight’s. The plugin’s stylesheet does the same in the few places it must.

Class hooks are stable and prefixed pyd-. The ones worth knowing:

Class What it wraps
.pyd-module A whole module page’s body
.pyd-member One documented object, including its members
.pyd-heading, .pyd-anchor An object’s heading and its permalink
.pyd-badges, .pyd-badge Kind, label and deprecation badges
.pyd-signature A signature block (--overload, --attribute variants)
.pyd-type A name inside a signature or annotation (linked or not)
.pyd-provenance, .pyd-source-link The line under a heading
.pyd-section, .pyd-section-title One docstring section and its heading
.pyd-params, .pyd-summary-table Parameter tables and member summaries
.pyd-aside An admonition (--tip, --caution, --danger)
.pyd-inherited The <details> block of members from a base class
.pyd-search… The symbol search element and its results
.pyd-page, .pyd-toc The built-in vanilla layout and its table of contents

For anything more selective, use the data attributes instead. They use the model’s own vocabulary: [data-pydocs-kind="class"], [data-pydocs-path="mypkg.Report"], [data-pydocs-group="methods"], [data-pydocs-section="parameters"], [data-pydocs-badge="deprecated"] and [data-pydocs-aside="note"].

Set any of these on :root, or on a narrower selector to retint one part of the site. The values shown are the defaults. Each uses Starlight’s token first, then a static fallback for sites without Starlight.

Token Default Used for
--pyd-text var(--sl-color-text, #24292f) Body text in signatures and tables
--pyd-text-muted var(--sl-color-gray-3, #57606a) Badges, provenance, section titles, source links
--pyd-text-invert var(--sl-color-black, #ffffff) Text on an accent background (the kind badge)
--pyd-bg var(--sl-color-bg, #ffffff) Page background in the built-in layout
--pyd-surface var(--sl-color-gray-6, #f6f8fa) Signature blocks, badges, search results
--pyd-border var(--sl-color-hairline-light, #d8dee4) Member rules, table borders, input borders
--pyd-accent var(--sl-color-text-accent, #2563eb) Linked types, anchors, the kind badge
--pyd-accent-low var(--sl-color-accent-low, #dbeafe) Accent backgrounds
--pyd-note var(--sl-color-blue, #3b82f6) Note asides
--pyd-note-low var(--sl-color-blue-low, #dbeafe) Note aside background
--pyd-tip var(--sl-color-purple, #8b5cf6) Tip asides
--pyd-tip-low var(--sl-color-purple-low, #ede9fe) Tip aside background
--pyd-caution var(--sl-color-orange, #ea580c) Caution asides and deprecation badges
--pyd-caution-low var(--sl-color-orange-low, #ffedd5) Caution aside background
--pyd-danger var(--sl-color-red, #dc2626) Danger asides
--pyd-danger-low var(--sl-color-red-low, #fee2e2) Danger aside background
--pyd-radius 0.375rem Corner radius on signatures and inputs
--pyd-mono var(--sl-font-mono, …) Every monospaced string

Because the defaults reference Starlight’s tokens, changing your site’s accent colour also changes the API pages. To make the API pages differ, retint one token at a time.

Under Starlight, there is nothing to do. The tokens follow --sl-*, and Starlight flips those.

Without Starlight, the stylesheet defines a dark palette twice, so both ways of choosing a theme work: once under @media (prefers-color-scheme: dark), guarded by :root:not([data-theme='light']), and once under :root[data-theme='dark']. Set data-theme="dark" or data-theme="light" on <html> from your own theme toggle, and the API pages follow it. Leave it unset, and they follow the operating system.

The site’s own pipeline highlights fenced blocks and doctest transcripts in a docstring, because docstring prose renders through the Markdown processor your project configured. There is no separate Shiki configuration here, and nothing to keep in sync.

Under Starlight, that means Expressive Code. A docstring’s code block gets the same frame, copy button and themes as a code block in a hand-written page. In a plain Astro project, it is Shiki’s .astro-code output, configured by markdown.shikiConfig:

astro.config.mjs
export default defineConfig({
markdown: {
shikiConfig: {
themes: { light: 'github-light', dark: 'github-dark' },
defaultColor: false,
},
},
});

Two themes with defaultColor: false emit both palettes as custom properties. This is what lets a single stylesheet work in both light and dark. This site uses that configuration.

Signatures are not Shiki output, but they are Shiki coloured. The model builds them, so that every type can be a link, and the tokens are then given the colours of the same themes you configure above. A cross-reference keeps its own .pyd-type colour through that pass, so a linked dict[str, Report] still reads as a link rather than disappearing into the grammar.

The colours are worked out once, at astro:config:done, and stored beside the extracted dump. Shiki cannot resolve a theme from inside the page-rendering bundle, where Astro has tree-shaken the theme registry away, so nothing is highlighted at render time.