New in 0.7
Palettes
Colour without re-rendering
silverpoint never writes a colour into its drawing: every stroke and fill is painted by CSS through the ground's --sp- custom properties. A palette sets them, so it re-colours a chart, a UI component, a dashboard, a composed component or the timeline without re-rendering it. palette= takes a preset name, a Palette, a dict of fields or a state var holding one.
import { sanguine } from '@silverpoint/core/palettes'; // npm install @silverpoint/core
import { SilverpointProvider } from '@silverpoint/react';
import { BarChart } from '@silverpoint/react/bar-chart';
import { SpButton } from '@silverpoint/react/ui/button';
<SilverpointProvider palette={sanguine}> {/* a whole section */}
<BarChart title="Orders" />
<SpButton variant="primary" palette={{ ink: '#2C6355' }}>Save</SpButton> {/* one field changed */}
</SilverpointProvider><script setup lang="ts">
import { sanguine } from '@silverpoint/core/palettes';
import { SilverpointProvider } from '@silverpoint/vue';
import { SpBarChart } from '@silverpoint/vue/bar-chart';
import { SpButton } from '@silverpoint/vue/ui/button';
</script>
<template>
<!-- or provideSilverpoint({ palette: sanguine }) for the whole app -->
<SilverpointProvider :palette="sanguine">
<SpBarChart title="Orders" />
<SpButton variant="primary" :palette="{ ink: '#2C6355' }">Save</SpButton>
</SilverpointProvider>
</template>// app.config.ts: the whole app (or a component's providers: one section of it)
import { type ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { sanguine } from '@silverpoint/core/palettes';
import { provideSilverpoint } from '@silverpoint/angular';
export const appConfig: ApplicationConfig = {
providers: [provideZonelessChangeDetection(), provideSilverpoint({ palette: sanguine })],
};
// a template: one field changed
// <button spButton variant="primary" [palette]="{ ink: '#2C6355' }">Save</button>from reflex_silverpoint_react import PALETTES, Palette, bar_chart, check_palette, silverpoint_palette, sp_button
bar_chart(title="Orders", palette="sanguine") # a preset
sp_button("Save", variant="primary", palette={"ink": "#2C6355"}) # a dict of fields
bar_chart(title="Orders", palette=State.palette) # a state var holding a dict
MINE = Palette(substrate="#F2E8DC", ink="#8A3A24", ink_secondary="#6A4A3A", text="#4A2216")
assert not check_palette(MINE) # [] when every field meets its contrast threshold
silverpoint_palette(dashboard(...), sp_card(...), palette="indigo") # a whole sectionThe 6 presets
Named after historical drawing media, each verified by check_palette against the contrast thresholds of the data model.
silverpointpassesThe silverpoint ground's own inks on cream: what a chart draws with no palette.
| hour | hits |
|---|---|
| 00 | 18 |
| 04 | 11 |
| 08 | 42 |
| 12 | 64 |
| 16 | 57 |
| 20 | 30 |
ink on substrate 5.29:1
sanguinepassesRed chalk, as in Leonardo's and Andrea del Sarto's studies.
| hour | hits |
|---|---|
| 00 | 18 |
| 04 | 11 |
| 08 | 42 |
| 12 | 64 |
| 16 | 57 |
| 20 | 30 |
ink on substrate 6.40:1
sepiapassesSepia wash and bistre.
| hour | hits |
|---|---|
| 00 | 18 |
| 04 | 11 |
| 08 | 42 |
| 12 | 64 |
| 16 | 57 |
| 20 | 30 |
ink on substrate 6.29:1
iron_gallpassesIron-gall ink: the blue-black of manuscripts.
| hour | hits |
|---|---|
| 00 | 18 |
| 04 | 11 |
| 08 | 42 |
| 12 | 64 |
| 16 | 57 |
| 20 | 30 |
ink on substrate 7.73:1
verdigrispassesVerdigris, the copper green of illuminators.
| hour | hits |
|---|---|
| 00 | 18 |
| 04 | 11 |
| 08 | 42 |
| 12 | 64 |
| 16 | 57 |
| 20 | 30 |
ink on substrate 5.84:1
indigopassesIndigo, the dyers' and printers' blue.
| hour | hits |
|---|---|
| 00 | 18 |
| 04 | 11 |
| 08 | 42 |
| 12 | 64 |
| 16 | 57 |
| 20 | 30 |
ink on substrate 7.17:1
A whole section: silverpoint_palette
silverpoint_palette(*children, palette=...) colours every chart, UI component, dashboard and surface inside it. The wrapper is display: contents, so it takes no layout space, and an element's own palette always wins over its section's. Pick a preset:
| point | value |
|---|---|
| 1 | 118 |
| 2 | 124 |
| 3 | 121 |
| 4 | 132 |
| 5 | 140 |
| 6 | 136 |
| 7 | 129 |
| 8 | 142 |
| 9 | 151 |
| 10 | 147 |
| 11 | 155 |
| 12 | 162 |
| 13 | 158 |
| 14 | 171 |
| month | units |
|---|---|
| Jan | 120 |
| Feb | 98 |
| Mar | 143 |
class Theme(rx.State):
preset: str = "sanguine"
@rx.var
def palette(self) -> dict[str, str]:
return PALETTES[self.preset].to_dict()
@rx.event
def set_preset(self, name: str):
self.preset = name
sp_segmented(items=[ui_item(name, name) for name in PALETTES], value=Theme.preset, on_change=Theme.set_preset)
silverpoint_palette(dashboard(...), palette=Theme.palette) # re-colours without re-renderingBuild your own
Unset fields keep the ground's own colour. Pick the five that matter most; the preview reads them as a dict, as a state var would. Then measure the result with check_palette: it returns one message per field below its threshold, and [] when the palette passes (only hex colours can be measured; a palette without substrate is measured against cream, #EDE7DA). In JavaScript, checkPalette and contrastRatio on @silverpoint/core/palettes do the same:
| month | units |
|---|---|
| Jan | 120 |
| Feb | 98 |
| Mar | 143 |
Palette(
substrate="#F2E8DC",
ink="#8A3A24",
ink_secondary="#6A4A3A",
text="#4A2216",
text_muted="#7A3E2C",
)import { checkPalette, contrastRatio, sanguine } from '@silverpoint/core/palettes';
const mine = { ...sanguine, ink: '#5A1F14' }; // fields are camelCase: inkSecondary, textMuted
checkPalette(mine); // [] when every pair meets its threshold, else { pair, ratio, threshold, … }
contrastRatio('#5A1F14', '#F2E8DC'); // the WCAG ratio, 1 to 21Fields
| Field | Custom property | Minimum contrast on the substrate |
|---|---|---|
substrate | --sp-substrate | — (what the others are measured on) |
ink | --sp-ink | 4.5:1 |
ink_secondary | --sp-ink-secondary | 4.5:1 |
heighten | --sp-heighten | exempt (always outlined) |
rule | --sp-rule | 3.0:1 |
grid | --sp-grid | 3.0:1 |
text | --sp-text | 4.5:1 |
text_muted | --sp-text-muted | 4.5:1 |
focus | --sp-ui-focus-color | 3.0:1 |
Palette.to_dict(), to_css() and merged(**changes) convert and copy palettes; Palette.from_dict() rejects unknown fields. palette_css(palette) returns the declarations as a style dict, and contrast_ratio(fg, bg) is the WCAG ratio of two hex colours.
Rules
- Monochrome by design. Tone is built from hatch density, so a chart has two inks —
inkfor the main series andink_secondaryfor the second — never a colour per series. precisionmode and the system's high-contrast modes still apply on top of any palette.- React, Vue and Angular:
SilverpointProvider/provideSilverpointtake apalette; a nested provider, a dashboard cell or apaletteprop overrides it field by field. A value is accepted only if it is a CSS colour. - Reflex:
silverpoint_providertakes apalette, andsilverpoint_paletteis a provider with one. Overlays render in place and read it; onlysp_triggerdoes not, so give itpalettedirectly.
ironGall, inkSecondary and textMuted in JavaScript are iron_gall, ink_secondary and text_muted in Python. Both set the same --sp- custom properties, which you can also set in CSS (the Tailwind preset names them as tokens).