Concepts
UI components
Try it
Every control here is a silverpoint component bound to client-side state: the segmented control re-grounds the panel, the switch turns the inking off on the components and the charts alike, the slider drives the gauge and the progress bar.
The catalog
| Group | Components | |
|---|---|---|
| Actions | Button | A native button, or a link drawn as one. |
| Data entry | Input, Checkbox, RadioGroup, Switch, Slider, Rate, Segmented | Native inputs, checkboxes, radios and ranges in hand-drawn frames: forms submit natively. |
| Navigation | Tabs, Steps | Tabs with their panels and an ordered list of steps, with the WAI-ARIA keyboard patterns. |
| Data display | Card, Tag, Badge, Divider | Cards, toned tags, counting badges and captioned rules. |
| Feedback | Progress, Alert, Skeleton | Progress as a line or a circle, alerts by kind, and loading placeholders. |
The UI component reference has a page per component with every declared state live on both grounds, its code in the four frameworks and its props.
Install and import
The UI components ship in the same adapter packages as the charts, on the ui subpath. Their stylesheet is opt-in: import @silverpoint/grounds/ui.css once, beside styles.css. The Angular adapter takes @angular/forms as a peer, for its ControlValueAccessors.
import '@silverpoint/fonts/fonts.css';
import '@silverpoint/grounds/styles.css';
import '@silverpoint/grounds/ui.css';
// One subpath per component, or every one from the barrel:
import { SpButton } from '@silverpoint/react/ui/button';
import { SpInput, SpSwitch, SpTabs, SpTabPanel } from '@silverpoint/react/ui';<script setup lang="ts">
import '@silverpoint/fonts/fonts.css';
import '@silverpoint/grounds/styles.css';
import '@silverpoint/grounds/ui.css';
import { SpButton } from '@silverpoint/vue/ui/button';
import { SpInput, SpSwitch, SpTabs, SpTabPanel } from '@silverpoint/vue/ui';
</script>/* src/styles.css */
@import '@silverpoint/fonts/fonts.css';
@import '@silverpoint/grounds/styles.css';
@import '@silverpoint/grounds/ui.css';
// app.ts
import { SpButton } from '@silverpoint/angular/ui/button';
import { SpInput, SpSwitch, SpTabs, SpTabPanel } from '@silverpoint/angular/ui';# Nothing to import on the JavaScript side: every UI component adds
# @silverpoint/grounds/ui.css (and styles.css and the typeface) for you.
from reflex_silverpoint_react import sp_button, sp_input, sp_switch, sp_tab_panel, sp_tabsValues, forms and events
Value components (Input, Checkbox, RadioGroup, Switch, Slider, Rate, Segmented, Tabs) are controlled or uncontrolled. The change event carries the new value itself (a string, a bool or a number), never a DOM event. The native inputs submit under their name, so a plain form works. Tag and Alert take closable and emit a close event.
import { useState, type FormEvent } from 'react';
import { SpButton, SpInput, SpSwitch, SpTabPanel, SpTabs } from '@silverpoint/react/ui';
import { BarChart } from '@silverpoint/react/bar-chart';
import { LineChart } from '@silverpoint/react/line-chart';
export function Settings() {
const [city, setCity] = useState('Caracas');
const [precision, setPrecision] = useState(false);
const [view, setView] = useState('traffic');
const mode = precision ? 'precision' : 'ink';
const save = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(Object.fromEntries(new FormData(event.currentTarget))); // { city, precision, … }
};
return (
<form onSubmit={save}>
{/* onChange receives the value itself, not the DOM event */}
<SpInput label="City" name="city" value={city} onChange={setCity}
invalid={city === ''} message="Required: pick a city" />
<SpSwitch label="Precision" name="precision" checked={precision} onChange={setPrecision} />
<SpTabs label="Views" value={view} onChange={setView}
items={[{ key: 'traffic', label: 'Traffic' }, { key: 'errors', label: 'Errors' }]}>
<SpTabPanel value="traffic"><LineChart title="Traffic" mode={mode} /></SpTabPanel>
<SpTabPanel value="errors"><BarChart title="Errors" mode={mode} /></SpTabPanel>
</SpTabs>
<SpButton type="submit" variant="primary">Save</SpButton>
</form>
);
}<script setup lang="ts">
import { ref, computed } from 'vue';
import { SpButton, SpInput, SpSwitch, SpTabPanel, SpTabs } from '@silverpoint/vue/ui';
import { SpBarChart } from '@silverpoint/vue/bar-chart';
import { SpLineChart } from '@silverpoint/vue/line-chart';
const city = ref('Caracas');
const precision = ref(false);
const view = ref('traffic');
const mode = computed(() => (precision.value ? 'precision' : 'ink'));
const tabs = [{ key: 'traffic', label: 'Traffic' }, { key: 'errors', label: 'Errors' }];
function save(event: SubmitEvent) {
console.log(Object.fromEntries(new FormData(event.target as HTMLFormElement)));
}
</script>
<template>
<form @submit.prevent="save">
<SpInput v-model="city" label="City" name="city"
:invalid="city === ''" message="Required: pick a city" />
<SpSwitch v-model="precision" label="Precision" name="precision" />
<SpTabs v-model="view" label="Views" :items="tabs">
<SpTabPanel value="traffic"><SpLineChart title="Traffic" :mode="mode" /></SpTabPanel>
<SpTabPanel value="errors"><SpBarChart title="Errors" :mode="mode" /></SpTabPanel>
</SpTabs>
<SpButton type="submit" variant="primary">Save</SpButton>
</form>
</template>import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SpButton, SpInput, SpSwitch, SpTabPanel, SpTabs } from '@silverpoint/angular/ui';
import { SpBarChart } from '@silverpoint/angular/bar-chart';
import { SpLineChart } from '@silverpoint/angular/line-chart';
@Component({
selector: 'app-settings',
imports: [FormsModule, SpButton, SpInput, SpSwitch, SpTabs, SpTabPanel, SpBarChart, SpLineChart],
template: `
<form (ngSubmit)="save()">
<!-- [(value)] two-way binds a signal; ngModel and formControl work too (ControlValueAccessor) -->
<sp-input label="City" name="city" [(value)]="city"
[invalid]="city() === ''" message="Required: pick a city" />
<sp-switch label="Precision" name="precision" [(value)]="precision" />
<sp-tabs label="Views" [items]="tabs" [(value)]="view">
<sp-tab-panel value="traffic"><sp-line-chart title="Traffic" [mode]="mode()" /></sp-tab-panel>
<sp-tab-panel value="errors"><sp-bar-chart title="Errors" [mode]="mode()" /></sp-tab-panel>
</sp-tabs>
<button spButton type="submit" variant="primary">Save</button>
</form>
`,
})
export class Settings {
protected readonly city = signal('Caracas');
protected readonly precision = signal(false);
protected readonly view = signal('traffic');
protected readonly mode = computed(() => (this.precision() ? 'precision' : 'ink'));
protected readonly tabs = [{ key: 'traffic', label: 'Traffic' }, { key: 'errors', label: 'Errors' }];
save() { console.log(this.city(), this.precision()); }
}import reflex as rx
from reflex_silverpoint_react import (
bar_chart, line_chart, sp_button, sp_input, sp_switch, sp_tab_panel, sp_tabs, ui_item,
)
class State(rx.State):
city: str = "Caracas"
precision: bool = False
view: str = "traffic"
@rx.event
def set_city(self, value: str): # on_change gets the value, not an event
self.city = value
@rx.event
def set_precision(self, value: bool):
self.precision = value
@rx.event
def set_view(self, value: str):
self.view = value
@rx.event
def save(self, form: dict):
print(form) # {"city": "Caracas", ...}: the native inputs submit under their name
def settings() -> rx.Component:
mode = rx.cond(State.precision, "precision", "ink")
return rx.el.form(
sp_input(label="City", name="city", value=State.city, on_change=State.set_city,
invalid=State.city == "", message="Required: pick a city"),
sp_switch(label="Precision", name="precision", checked=State.precision,
on_change=State.set_precision),
sp_tabs(
sp_tab_panel(line_chart(title="Traffic", mode=mode), value="traffic"),
sp_tab_panel(bar_chart(title="Errors", mode=mode), value="errors"),
items=[ui_item("traffic", "Traffic"), ui_item("errors", "Errors")],
label="Views", value=State.view, on_change=State.set_view,
),
sp_button("Save", type="submit", variant="primary"),
on_submit=State.save,
)| Framework | Controlled | Uncontrolled | Close |
|---|---|---|---|
| React | value / checked + onChange | defaultValue / defaultChecked | onClose |
| Vue | v-model | default-value | @close |
| Angular | [(value)], ngModel, formControl | [defaultValue] | (close) |
| Reflex | value= / checked= + on_change= | default_value= / default_checked= | on_close= |
Items are plain objects: { key, label, disabled? } for Tabs, Segmented and RadioGroup, { key, title, description?, status? } for Steps. In Reflex, ui_item(key, label, disabled=False) and step_item(key, title, description=None, status=None) build them.
Grounds, modes and sizes
Every UI component takes ground, substrate, mode, seed and size. Like a chart's, they fall back to the dashboard the component sits in, then to the provider. size is sm, md (the default) or lg. The frame's hand derives from id, so the same id draws the same frame on server and client.
Keyboard and accessibility
- A native element underneath: a
<button>, an<input>, a<fieldset>of radios. Forms, autofill and assistive technology work as with plain HTML. - Tabs, Segmented, RadioGroup and Rate follow the WAI-ARIA patterns: one tab stop, the arrows move (and select), Home and End jump, disabled items are skipped, and
dir="rtl"mirrors the arrows. - One exact focus ring, drawn on the item, the box or the thumb; at most one heightened element per component.
- Alerts are
alert(warning, error) orstatus(info, success); Progress is aprogressbar; Skeleton setsaria-busy. An indeterminate Progress only moves when reduced motion is off. - Verified on the example apps with axe (A/AA), keyboard, native submit, target size and reduced motion.
Server rendering
The components render on the server like the charts. React also ships pure-server entry points for the components that hold no state: @silverpoint/react/server/ui/<name> for card, divider, steps, tag, badge, progress, alert and skeleton.
In Reflex
reflex-silverpoint-react wraps all 17: sp_button, sp_input, sp_checkbox, sp_radio_group, sp_switch, sp_slider, sp_rate, sp_segmented, sp_tabs, sp_steps, sp_card, sp_tag, sp_badge, sp_divider, sp_progress, sp_alert, sp_skeleton, plus sp_tab_panel. UI_COMPONENTS is the catalog (group, states, props), UI_DEMOS the 45 reference states, and ui_demo("radio-group", "selected", ground="cyanotype") renders one: this site's reference pages are built from them.
default_value and read the value on submit.This site has no backend (rx.App(enable_state=False)), so its components are bound to client-side state instead of an rx.State:
from reflex.experimental.client_state import ClientStateVar
from reflex_silverpoint_react import gauge_arc, sp_slider
# A site exported with enable_state=False has no backend: keep the value in the browser.
LOAD = ClientStateVar.create("load", 64)
sp_slider(label="Load", value=LOAD.value, on_change=LOAD.set_value, marks=[0, 25, 50, 75, 100])
gauge_arc(percent=LOAD.value, caption="Load", title="Capacity")Props every component takes
| React / Angular | Vue | Reflex | Type | Description |
|---|---|---|---|---|
id | id | id | str | The frame variant and every related id derive from it. |
ground | ground | ground | Literal['silverpoint', 'cyanotype'] | str | dict[str, Any] | Style ground: "silverpoint" or "cyanotype", or a ground object. Defaults to the dashboard's, then the provider's, or silverpoint. |
substrate | substrate | substrate | Literal['cream', 'green', 'blue', 'ochre', 'prussian'] | Prepared substrate within the ground. |
mode | mode | mode | Literal['ink', 'precision'] | ink by default; precision disables inking. |
seed | seed | seed | str | int | Seed of the frame's hand; overrides id for the frame variant only. |
size | size | size | Literal['sm', 'md', 'lg'] | Control size: sm, md (default) or lg. |
palette | palette | palette | dict[str, str] | Colours over the ground's (set with palette=), merged over the dashboard's and the provider's. |
className | class-name | class_name | str | Added to the component's root element, after its own classes. |