Interactive data apps in Go.
Syralit is a Go-native framework for building interactive data apps, dashboards, and AI tool interfaces — inspired by Streamlit, designed for Go.
Write Go functions, get a live web app. No JavaScript, no HTML templates, no frontend build step.
package main
import sy "github.com/HazelnutParadise/syralit"
func main() {
sy.App(func() {
sy.Title("Hello Syralit")
name := sy.TextInput("Your name")
if name != "" {
sy.Success("Hello, " + name + "!")
}
})
}go install github.com/HazelnutParadise/syralit/cmd/syralit@latest- Go 1.25+
syralit new myapp # scaffold a new project
cd myapp
syralit dev # hot reload with state preservationOr manually:
package main
import sy "github.com/HazelnutParadise/syralit"
func main() {
sy.App(func() {
sy.Title("My App")
if sy.Button("Click me") {
sy.Balloons()
}
})
}go run .
# Open http://localhost:8600The skills/ directory contains Agent Skills for building Syralit apps with AI coding assistants.
syralit-dev: complete Syralit API referencesyralit-artifact-dsl: specialized guide for generating valid Artifact DSL JSON
Install it into your project with the skills CLI:
npx skills add HazelnutParadise/syralit/skillsOr copy the skills/syralit-dev/ folder into your agent's skills directory manually (e.g. .claude/skills/).
Real screenshots captured from the runnable examples in examples/.
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
| Light | Dark |
|---|---|
![]() |
![]() |
Things Syralit does that Streamlit can't, by leaning on Go:
// Background jobs — run work in a goroutine; the page stays responsive and the
// server pushes the result when ready (a Streamlit rerun blocks the whole app).
job := sy.Task("report", func() Report { return buildReport() }) // runs once
if job.Running() {
sy.Spinner("Crunching…")
} else {
render(job.Result())
}sy.Task[T]— non-blocking background work with auto-push on completion.sy.Shared[T]— app-wide state shared across all sessions; aSet/Updatepushes a live update to every connected client (real-time collaboration).sy.ArtifactCanvas— an agent-updatable canvas region rendered from a controlled DSL of reusable Syralit components. Apps opt in to a POST endpoint with bearer-token auth; new specs animate into place for every open session.sy.Fragment(key, fn, sy.RunEvery(d))— server-driven live refresh.syralit build— compile the whole app (front-end + backend + yourpublic/) into one self-contained executable; no Python, no runtime, no deps.- Fully offline / air-gapped —
sy.SetAssetURL(name, url)repoints any third-party lib (Chart.js, Leaflet, KaTeX, Plotly, …) to a self-hosted copy; drop the files inpublic/andsyralit buildbakes everything into one binary that needs no internet or CDN (also satisfies strict CSP). - Automatic SSE fallback — when a WebSocket can't be established (e.g. a proxy that blocks WS upgrades), the client transparently switches to a plain HTTP transport: Server-Sent Events downstream + POST upstream. No code change.
sy.Handler(cfg, fn)— mount a Syralit app as a plainhttp.Handlerinside an existing Go server (works behindhttp.StripPrefixsub-paths).- Native desktop apps — ship the exact same app code as a desktop window
via
integrations/desktop(Wails v3):sydesktop.App(fn)instead ofsy.App(fn), and the Go code runs on the user's machine with direct local file access. Packaging a Streamlit app as a desktop binary is famously painful; here it's one import swap. - Typed state via generics (
sy.State[T]) and typedsy.Task[T]results.
| Widget | Returns | Description |
|---|---|---|
Button |
bool |
Clickable button (true for one rerun) |
TextInput |
string |
Single-line text |
PasswordInput |
string |
Masked text input |
TextArea |
string |
Multi-line text |
NumberInput |
float64 |
Number with min/max/step |
Slider |
float64 |
Range slider |
RangeSlider |
(float64, float64) |
Two-handle slider returning a (low, high) range |
DateSlider |
string |
Slider over a date range, returns "YYYY-MM-DD" |
TimeSlider |
string |
Slider over a time range, returns "HH:MM" |
SelectSlider |
string |
Discrete slider with labels |
Checkbox |
bool |
Checkbox |
Toggle |
bool |
Toggle switch |
Radio |
string |
Radio button group |
SelectBox |
string |
Dropdown (auto-searchable at 20+ items) |
MultiSelect |
[]string |
Multi-select dropdown |
DateInput |
string |
Date picker (YYYY-MM-DD) |
DatetimeInput |
string |
Date+time picker (YYYY-MM-DD HH:MM) |
DateRangeInput |
(string, string) |
Start/end date pickers |
TimeInput |
string |
Time picker (HH:MM) |
ColorPicker |
string |
Color hex picker |
FileUploader |
*UploadedFile |
File upload |
FileUploaderMultiple |
[]*UploadedFile |
Multi-file upload |
CameraInput |
string |
Webcam capture |
AudioInput |
string |
Microphone recording |
ChatInput |
string |
Chat message input |
Feedback |
string |
Thumbs up/down |
SegmentedControl |
string |
Segmented buttons (SegmentedControlMulti → []string) |
Pills |
string |
Pill-style buttons (PillsMulti → []string) |
Pagination |
int |
Page selector |
MenuButton |
string |
Dropdown button returning the clicked option |
Plus: DownloadButton, LinkButton, PageLink, Badge.
Title, Header, Subheader, Text, Textf, Markdown, Caption, Code (syntax highlighting via highlight.js), LaTeX (KaTeX), JSON (interactive tree), HTML, Image, ImageFromBytes, Audio, Video, Link, Metric (with delta indicators), Progress, Spinner, WriteStream (token-by-token streaming), Component (custom HTML/JS), IFrame, PDF (embedded viewer), Exception (styled Go error box).
ArtifactCanvas renders a shared, animated canvas from a safe DSL. The DSL is
designed for AI agents: it accepts only a curated set of reusable Syralit
components, supports JSON Pointer data binding, and never exposes raw HTML,
custom JS, iframes, or the internal Node protocol.
mainBoard := sy.NewArtifactStore("main", sy.ArtifactSpec{
Version: "v1",
Layout: sy.ArtifactLayout{Columns: 2, Gap: 14, Padding: 16},
Data: map[string]any{
"summary": map[string]any{"revenue": "$42k"},
},
Nodes: []sy.ArtifactNode{{
ID: "revenue",
Component: "metric",
Props: map[string]any{"label": "Revenue"},
Bind: map[string]string{"props.value": "/summary/revenue"},
}},
})
notesBoard := sy.NewArtifactStore("notes", notesSpec)
auth := sy.StaticAgentKey("local-agent", sy.Secrets("AGENT_KEY"))
// One discoverable endpoint for every explicitly exposed canvas.
sy.HandleArtifactAPI(
"/api/agent/artifacts",
auth,
mainBoard,
notesBoard,
)
sy.App(func() {
sy.ArtifactCanvas(mainBoard, sy.Height(520))
sy.ArtifactCanvas(notesBoard, sy.Height(240))
})Set SYRALIT_URL to the public URL printed by syralit run or syralit dev.
Agents then discover the available canvases and update one by ID. The discovery
response also carries a components object (builtin + custom) so an agent
can tell whether an opt-in component like insyra is enabled before using it:
curl "$SYRALIT_URL/api/agent/artifacts" \
-H "Authorization: Bearer $AGENT_KEY"
curl -X POST "$SYRALIT_URL/api/agent/artifacts" \
-H "Authorization: Bearer $AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"artifact":"main","expected_revision":1,"spec":{"version":"v1","nodes":[{"id":"msg","component":"text","props":{"text":"Updated"}}]}}'The update response includes a revision plus page/selector preview metadata.
Browser-capable agents wait until the selected canvas has the returned
data-artifact-revision and data-artifact-state="settled", then capture that
element. This waits for the keyed transition, charts, images, and fonts rather
than taking a screenshot mid-animation.
expected_revision must match the latest discovery/current-spec response;
stale writes receive 409 Conflict instead of silently overwriting a newer
agent update.
In hot-reload mode, the public dev server proxies /api/ to its current child;
never use the ephemeral child port shown in internal diagnostics.
HandleArtifactEndpoint remains available when each canvas needs its own route
or authenticator. ArtifactAPIHandler and ArtifactHandler return ordinary
http.Handler values for mounting the API on another mux or port.
For user-managed keys, implement sy.AgentKeyStore and render
sy.AgentKeyManager(store). Syralit provides the UI and callback contract; your
app decides whether keys live in memory, a file, a database, or another secret
system.
Full DSL reference: docs/artifact-dsl.md
| Widget | Description |
|---|---|
Table |
Static string table |
DataFrame |
Sortable table; optional row selection (sy.Selectable() → returns selected indices; sy.SelectionMode("single-row") for one), sy.ColumnOrder(...) to reorder/filter columns, typed display via sy.ColConfig |
DataEditor |
Editable table with 11 column types |
Column configuration (sy.ColConfig, shared by DataFrame and DataEditor) supports types text, number, checkbox, select, date, time, datetime, link, image, progress, list, plus the display-only mini-chart columns bar_chart / line_chart (cell value is a []float64). Each column may set Format (printf-style, e.g. "$%.2f", "%d%%"), Label (header override), Help (header tooltip), Width, Min/Max/Step, and Color (chart columns). Dynamic row add/delete with sy.DynamicRows().
sy.DataEditor(headers, rows,
sy.ColConfig(map[string]sy.ColumnConfig{
"Score": {Type: "number", Min: 0, Max: 100},
"Pass": {Type: "checkbox"},
"Grade": {Type: "select", Options: []string{"A", "B", "C"}},
}),
sy.DynamicRows(),
)Built-in interactive charts powered by Chart.js:
| Chart | Input | Description |
|---|---|---|
LineChart |
map[string][]float64 |
Line chart with multiple series |
BarChart |
map[string][]float64 |
Bar chart |
AreaChart |
map[string][]float64 |
Filled line chart |
ScatterChart |
map[string][][2]float64 |
Scatter plot with xy pairs |
PieChart |
map[string]float64 |
Pie chart |
DoughnutChart |
map[string]float64 |
Doughnut chart |
HistogramChart |
[]float64, bins |
Histogram from raw data |
RadarChart |
labels, map[string][]float64 |
Radar/spider chart |
GraphvizChart |
dot string |
Graphviz DOT via viz.js |
Bar/area/line charts accept sy.Stacked(), sy.Horizontal() (bar), sy.Colors([]string{...}), sy.XLabels(...), and sy.ChartTitle(...).
Line/Bar/Area/Scatter/Pie charts are selectable: add sy.Selectable() and the call returns a *sy.ChartSelection (Series/Index/X/Value) when the user clicks a data point:
if sel := sy.BarChart(data, sy.Selectable(), sy.Key("sales")); sel != nil {
sy.Textf("%s at %s = %v", sel.Series, sel.X, sel.Value)
}With sy.RangeSelectable(), dragging across a Line/Bar/Area chart selects an
x-axis range (sel.Range, Index..EndIndex, X..EndX) — the building
block for time-series drill-downs:
if sel := sy.LineChart(data, sy.RangeSelectable(), sy.Key("ts")); sel != nil && sel.Range {
sy.Textf("from %s to %s", sel.X, sel.EndX)
}External charting library integrations (CDN-loaded, accepting JSON specs):
| Chart | Library | Streamlit Equivalent |
|---|---|---|
VegaLiteChart |
Vega-Lite / vega-embed | st.altair_chart |
PlotlyChart |
Plotly.js | st.plotly_chart |
PyplotChart |
SVG/PNG images | st.pyplot |
BokehChart |
BokehJS | st.bokeh_chart |
PydeckChart |
deck.gl | st.pydeck_chart |
// Columns (equal or weighted)
cols := sy.Columns(3)
cols[0](func() { sy.Text("Col 1") })
cols := sy.WeightedColumns(2, 1, 1)
// Tabs
tab := sy.Tabs([]string{"Tab1", "Tab2"})
tab("Tab1", func() { sy.Text("Content 1") })
// Other containers
sy.Sidebar(func() { ... })
sy.Expander("Title", func() { ... })
sy.Container(func() { ... }, sy.Border())
sy.Form("key", func() { ... }, sy.ClearOnSubmit()) // ClearOnSubmit optional
sy.Status("Loading", "running", func() { ... })
sy.Fragment("key", func() { ... }) // partial rerun
sy.Space(sy.Height(32)) // vertical whitespace
sy.Bottom(func() { ... }) // pin content to the bottom of the viewport// Typed state (persists across reruns)
count := sy.State("count", 0)
count.Get()
count.Set(42)
// Query parameters (SetQueryParam updates the browser URL — shareable state)
val := sy.QueryParam("page")
sy.SetQueryParam("page", "2")
// Reset a widget to its default (e.g. clear a chart selection)
sy.ResetWidget("key")
// Request context (headers, cookies, host, IP, locale) — st.context
ctx := sy.Context()
lang := ctx.Locale
// Flow control
sy.Stop() // halt rendering
sy.Rerun() // force rerunfunc init() {
sy.AddPage("Home", homePage, sy.PageIcon("🏠"), sy.PageOrder(1))
sy.AddPage("About", aboutPage, sy.PageIcon("ℹ️"), sy.PageOrder(2))
}
func main() { sy.App(nil) }// Login gate blocks rendering until authenticated
username := sy.LoginGate(func(user, pass string) bool {
return user == "admin" && pass == "secret"
})
// Role-based access
user := sy.User() // map[string]string or nil
sy.Login(map[string]string{"name": "admin", "role": "admin"})
sy.Logout()OIDC single sign-on lives in a separate module (integrations/oidc) so
its dependency tree never touches the core. Wrap the app handler and every
visitor signs in through Google / Microsoft Entra / Keycloak / Auth0 / any
OIDC provider; sy.User() then returns the verified claims:
import syoidc "github.com/HazelnutParadise/syralit/integrations/oidc"
handler, _ := syoidc.Protect(sy.Handler(sy.Config{}, app), syoidc.Config{
Issuer: "https://accounts.google.com",
ClientID: os.Getenv("OIDC_CLIENT_ID"),
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
RedirectURL: "http://localhost:8600/auth/callback",
CookieSecret: []byte(os.Getenv("COOKIE_SECRET")),
})
http.ListenAndServe(":8600", handler)data := sy.CacheData("key", func() []Row {
return fetchFromDB()
}, sy.TTL(5 * time.Minute))
db := sy.CacheResource("db", func() *sql.DB {
return openDB()
})sy.Success("Done!")
sy.Error("Failed!")
sy.Warning("Watch out")
sy.Info("Note")
sy.Toast("Message", "success")
sy.Balloons()
sy.Snow()
sy.Dialog("Settings", func() { ... })sy.WriteStream(func(yield func(string)) {
for _, word := range words {
yield(word + " ")
time.Sleep(30 * time.Millisecond)
}
})msgs := sy.State("msgs", []map[string]string{})
for _, m := range msgs.Get() {
sy.ChatMessage(m["role"], func() {
sy.Markdown(m["content"])
})
}
if input := sy.ChatInput("Ask something..."); input != "" {
msgs.Set(append(msgs.Get(), map[string]string{
"role": "user", "content": input,
}))
}sy.Map([]sy.MapPoint{
{Lat: 25.0330, Lon: 121.5654, Text: "Taipei 101"},
}, sy.Height(450))db := sy.Connection("mydb")
rows := sy.SQLQuery(db, "SELECT * FROM users")sy.Key("unique_key") // stable widget identity
sy.DefaultValue(val) // initial value
sy.Placeholder("hint") // placeholder text
sy.Help("tooltip") // help tooltip
sy.Disabled() // disable widget
sy.Min(0), sy.Max(100) // numeric range
sy.Step(0.5) // numeric step
sy.Height(300), sy.Width(400)
sy.ChartTitle("Title") // chart title
sy.Border() // container border
sy.Color("green") // element color
sy.Language("go") // code language
// Button styling (Button, LinkButton, DownloadButton)
sy.Icon("🚀") // prefix a button label with an icon
sy.ButtonType("secondary") // "primary" (default), "secondary", "tertiary"
sy.UseContainerWidth() // make a button span its container
sy.Border() // also: bordered Metric card
sy.MinDate("2026-01-01") // DateInput / DateRangeInput lower bound
sy.MaxDate("2026-12-31") // DateInput / DateRangeInput upper boundSyralit has first-class support for Insyra DataTable and DataList via a cleanly separated adapter package. The core framework never imports Insyra.
import syi "github.com/HazelnutParadise/syralit/integrations/insyra"
// DataTable (multi-column)
syi.Table(dt) // render DataTable
syi.Preview(dt, 5) // first N rows
syi.EditableTable(dt, sy.Key("edit")) // editable DataTable
col := syi.ColumnSelect("Column", dt) // column picker
syi.Metrics(dt, col) // count, mean, min, max
syi.BarChart(dt, "Category", "Value") // chart from columns ("Category" = axis labels)
syi.LineChart(dt, "Month", "Revenue") // all chart helpers accept sy.Option and return
syi.ScatterChart(dt, "X", "Y") // *sy.ChartSelection when sy.Selectable() is set
syi.MultiLineChart(dt, "Month", nil) // every numeric column as a series (st.line_chart(df))
// Click-to-filter dashboards: GroupBy chart + selection + filter
sel := syi.GroupedBarChart(dt, "Region", "Revenue", insyra.OpSum,
sy.Selectable(), sy.Key("by_region"))
syi.Table(syi.FilterBySelection(dt, "Region", sel)) // nil selection = unfiltered
sub := syi.FilterEquals(dt, "Region", "North") // pure data helper
edited := syi.EditableDataTable(dt, sy.Key("e")) // edits → new *insyra.DataTable
syi.DownloadCSV("Export", dt, "data.csv") // CSV download button
syi.RollingMeanChart(dt, "Month", "Revenue", 7) // raw + rolling mean overlay
syi.CumSumChart(dt, "Month", "Revenue") // cumulative sum
syi.PctChangeChart(dt, "Month", "Revenue", 1) // percent change bars
out := syi.AddFormulaColumn(dt, "ccl") // interactive CCL formula column
// DataList (single series) — the symmetric counterpart
syi.List(dl) // single-column table
syi.ListPreview(dl, 5) // first N values
syi.EditableList(dl, sy.Key("edl")) // editable single column → []any
syi.ListMetrics(dl) // count, mean, min, max
syi.ListDescribe(dl) // count/mean/std/min/25%/50%/75%/max
syi.ListBarChart(dl) // value over index
syi.ListLineChart(dl)
syi.ListAreaChart(dl)
syi.Histogram(dl, 20) // distribution (list-only)
// Statistical analysis (insyra/stats), rendered in the UI
syi.Describe(dt) // full per-column summary table
syi.Correlation(dt, "X", "Y", "pearson") // r + p as metrics
syi.CorrelationMatrix(dt, "pearson") // pairwise correlation matrix
syi.LinearRegression(dt, "Y", "X1", "X2") // R²/coeffs table + scatter
syi.TTest(dt, "A", "B", false) // two-sample t-test
// Load a file into a DataTable (CSV / Excel / JSON)
dt := syi.UploadTable("Upload data") // file uploader → *DataTable
dt, err := syi.ParseTable(name, bytes) // parse bytes from any source
// Interactive transforms (non-destructive)
out := syi.FilterBuilder(dt) // column/op/value row filter
out := syi.CCLBuilder(dt) // add a computed column (CCL)Native interactive charts beyond the built-in Chart.js layer (Sankey, word cloud, K-line, gauge, funnel, …) live in a separate opt-in subpackage, because they pull in go-echarts and (transitively) chromedp:
import syiplot "github.com/HazelnutParadise/syralit/integrations/insyra/eplot"
import "github.com/HazelnutParadise/insyra/plot"
syiplot.WordCloud(dl, "Tags") // no Chart.js equivalent
syiplot.EChart(plot.CreateSankeyChart(cfg, links...)) // any insyra/plot chart
syiplot.SetOffline(true) // inline echarts JS into each chart — no CDN, runs
// air-gapped / under strict CSP / as a `syralit build` binaryRun the Insyra CLI DSL (.isr) from Go and render the result, or let agents
embed a DSL script directly in an Artifact for live server-side computation.
This is a separate opt-in subpackage because the DSL engine pulls in the full
Insyra CLI dependency tree (cobra, database drivers, parquet/arrow, readline):
import syidsl "github.com/HazelnutParadise/syralit/integrations/insyra/insyradsl"
// Go widget: run a script (safe mode) and auto-render. Cached by script hash.
syidsl.DSL(`
newdl Q1 Q2 Q3 Q4 as quarter
newdl 42 55 61 78 as revenue
newdt quarter revenue as t
setcolnames t quarter revenue
`, syidsl.Render("bar_chart"), syidsl.Output("t"), syidsl.X("quarter"), syidsl.Y("revenue"))
res := syidsl.RunDSL(script) // low-level: inspect res.Vars / res.Output / res.ErrRunDSL runs in safe mode by default — a default-deny allowlist of pure,
in-memory compute commands; load/save/db/fetch/run are rejected, and
each run uses an isolated, ephemeral environment (no shared ~/.insyra state).
Pass syidsl.Unrestricted() only for trusted, app-authored scripts.
Importing the package also registers an insyra Artifact component, so an
agent can POST a spec that computes live instead of only binding static data:
{ID: "chart", Component: "insyra", Props: map[string]any{
"script": "newdl North South West as region\nnewdl 12 18 9 as deals\n" +
"newdt region deals as t\nsetcolnames t region deals",
"render": "bar_chart", "output": "t", "x": "region", "y": "deals"}}integrations/desktop ships a Syralit app as a native desktop window
(Wails v3). It is a separate Go module — webapps never
pull the Wails dependency tree:
import (
sy "github.com/HazelnutParadise/syralit"
sydesktop "github.com/HazelnutParadise/syralit/integrations/desktop"
)
func main() {
sydesktop.App(func() {
sy.Title("My tool")
// ... exactly the same code as a webapp
}, sydesktop.WindowSize(1200, 800), sydesktop.MinSize(640, 480))
}The app serves on a loopback-only random port, the window renders it through
the OS webview, and closing the window shuts everything down. Options:
WindowTitle, WindowSize, MinSize, Frameless, Icon(pngBytes),
Config(sy.Config), AllowBrowser(). sydesktop.Run is the error-returning
variant. Because the Go process runs on the user's machine, local files are
directly readable — no upload round-trip (see examples/desktop-demo).
By default the server is locked to its window: requests without the
window's per-launch token get 403, so other local browsers can't open the app
(pass AllowBrowser() to permit it). /api/ endpoints are exempt — agent
artifact endpoints keep working with their own bearer auth. The app's
environment gets SYRALIT_URL so agent subprocesses it spawns can find those
endpoints, and an explicit Config(sy.Config{Port: N}) pins the port when
external agents need a stable address.
Build requirements (Wails v3's): nothing extra on Windows (WebView2 ships with
Windows 10/11), Xcode command-line tools on macOS, webkit2gtk on Linux. For
icons/installers use the wails3 CLI packaging tooling.
Hot reload: syralit dev works for desktop apps too — the native window
attaches to the dev supervisor and survives rebuilds like a browser tab would
(session state preserved, build errors overlay in the window). Closing the
window leaves the dev session running; it auto-closes when the supervisor
stops.
title = "My App"
host = "0.0.0.0"
port = 8600
[theme]
mode = "system" # "light" | "dark" | "system"
accent = "#7C3AED"
radius = "12px"
button_radius = "999px" # defaults to radius
background_color = "#ffffff"
secondary_background_color = "#f8f9fb" # widget/code/sidebar surface
text_color = "#1f2329"
link_color = "#2563eb" # defaults to accent
link_underline = true # true: always, false: never, unset: on hover
code_text_color = "#0f766e"
code_background_color = "#f1f5f9"
border_color = "#e5e7eb"
dataframe_border_color = "#e5e7eb"
dataframe_header_background_color = "#f3f4f6"
show_widget_border = true
show_sidebar_border = true
# Basic palette (badges, alerts, status colors); each of red/orange/yellow/
# blue/green/violet/gray also has <name>_background_color and <name>_text_color.
red_color = "#dc2626"
blue_background_color = "#eff6ff"
# Chart palettes (categorical drives built-in chart series colors).
chart_categorical_colors = ["#7c3aed", "#2563eb", "#16a34a"]
chart_sequential_colors = ["#f0fdfa", "#0f766e"]
chart_diverging_colors = ["#dc2626", "#f8fafc", "#2563eb"]
# Fonts: "sans-serif" | "serif" | "monospace" select the built-in Source Sans 3 /
# Source Serif 4 / Source Code Pro (embedded, served locally — no CDN); any
# other value is used as a CSS font-family list.
font = "sans-serif"
heading_font = "serif"
code_font = "monospace"
base_font_size = 16
base_font_weight = 400
heading_font_sizes = ["2rem", "1.5rem", "1.15rem"] # h1..h6
heading_font_weights = [700, 650, 600]
code_font_size = "0.875rem"
code_font_weight = 400
[[theme.font_faces]] # load custom fonts (otf/ttf/woff/woff2)
family = "Inter"
url = "/fonts/inter.woff2" # public/ path or absolute URL
weight = "100 900"
style = "normal"
[theme.sidebar] # sidebar-only overrides — every color/font/radius key
font = "sans-serif" # above works here too (inherits main theme when unset)
accent = "#f59e0b"
[secrets]
api_key = "sk-..."
db_dsn = "postgres://..."
[server]
max_upload_size_mb = 50 # FileUploader/CameraInput cap (default 10 MB)
ssl_cert_file = "cert.pem" # serve HTTPS when both are set
ssl_key_file = "key.pem"
[i18n] # localize built-in UI text
connecting = "連線中…"
loading = "載入中…"sy.SetPageConfig(
sy.PageTitle("My App"),
sy.PageLayout("wide"),
sy.ConfigIcon("🚀"),
sy.PrimaryColor("#ff4b4b"),
sy.InitialSidebarState("collapsed"),
sy.ConfigMenuItems("https://…/help", "https://…/issues", "**About** markdown"),
)
apiKey := sy.Secrets("api_key")| Command | Description |
|---|---|
syralit new <name> |
Scaffold a new project in a new folder |
syralit new . |
Scaffold into the current directory (no wrapper folder) |
syralit dev |
Hot reload with state preservation |
syralit run |
Build and run once (no watching) |
syralit build [-o out] [dir] |
Compile to a single self-contained executable |
Drop files in a public/ directory and they're served at the site root —
public/logo.png → /logo.png. In syralit dev they're served from disk; for
production, syralit build folds public/ (and any assets/ overrides) into
the binary via //go:embed, so the result is one executable with the
front-end, backend, and all your static files — nothing to copy alongside it.
syralit build # → ./<dir-name>[.exe], everything embedded
syralit build -o myapp . # custom output pathYou can also wire static files manually with sy.Static(fsys) (served at the
root) and sy.StaticAssets(fsys) (overrides the built-in front-end assets).
sy.AppTest is a headless testing harness (the Go counterpart of Streamlit's
st.testing.v1.AppTest): render without a server, simulate widget input and
button clicks, and assert on the resulting tree.
at := sy.NewAppTest(func() {
name := sy.TextInput("Name", sy.Key("name"))
if sy.Button("Greet", sy.Key("greet")) {
sy.Success("Hello, " + name)
}
})
at.Run()
at.SetValue("name", "Ada")
at.Click("greet") // or at.ClickLabel("Greet")
at.Run()
// at.Texts("status") -> ["Hello, Ada"]For a single render, sy.RenderOnce(appFn) *Node returns the UI tree directly:
tree := sy.RenderOnce(func() { sy.Metric("Users", "24,891") })
if len(tree.Find("metric")) != 1 { t.Fatal("expected a metric") }The repo also ships a browser-level UI suite under uitest/ — a
separate Go module (so its chromedp dependency never touches the framework's
go.mod) that drives headless Chrome against an in-process app and asserts on
real rendered behavior: widget round-trips over WebSocket, CSS visibility,
canvas chart clicks, multi-file uploads, and sub-path mounting.
cd uitest && go test ./... # requires a local Chrome/ChromiumThe desktop module has its own e2e suite (Windows) that opens real native
windows: production launch + browser lockdown + agent artifact auth + graceful
close, and syralit dev hot reload with the window surviving rebuilds. It is
gated so plain test runs stay headless:
cd integrations/desktop
SYRALIT_DESKTOP_E2E=1 go test ./... # opens real windows brieflyThe examples/ directory contains runnable demo apps:
| Example | Description |
|---|---|
hello |
Minimal single-page app with basic widgets |
showcase |
Comprehensive 6-page demo of all features |
chatbot |
Chat UI with simulated streaming AI responses |
form-app |
Conference registration form with validation |
data-explorer |
3-page sales dashboard with charts, filters, and data editing |
auth-demo |
Authentication with LoginGate and role-based access control |
agent-artifact |
Artifact Canvas studio with multiple live boards, preset scenarios, local compose controls, and agent endpoints |
mega-demo |
10-page app showcasing every feature: all widgets, charts, layout, forms, data tables, chat, maps, state |
insyra-demo |
Insyra integration: tables, stats, transforms, file upload, native charts |
insyra-charts |
Native go-echarts charts (Sankey/gauge/funnel/word cloud) with offline inlining |
insyra-artifact |
Insyra DSL dynamic computation: syidsl.DSL widget and the agent-driveable insyra Artifact component |
embed-scroll |
Themed scrollbars inside embedded Component iframes (follow light/dark) |
theme-fonts |
Theming: built-in Source fonts, custom @font-face, palette/link/button-radius/chart colors, sidebar overrides |
streamlit-parity |
PDF viewer, multi-file upload, collapsed sidebar, app menu, free-entry MultiSelect, clipped media |
insyra-interactive |
Click-to-filter dashboard: selectable GroupBy chart drives an Insyra-filtered table, metrics, and deep-linkable URL state |
data-studio |
Full upload → explore workflow: file upload, column pickers, aggregate switch, selectable GroupBy chart, drill-down detail and statistics |
oidc-login |
OIDC single sign-on via integrations/oidc (standalone module) |
desktop-demo |
Native desktop window via integrations/desktop (standalone module): same app code, direct local file access |
Run any example:
cd examples/chatbot
go run .Syralit covers the commonly-used Streamlit surface in idiomatic Go. See docs/STREAMLIT_PARITY.md for the full mapping and the few intentional gaps.
See CHANGELOG.md.
MIT









