Skip to content

zero-sq/slintcn

slintcn

Pages crates.io npm License: MIT

shadcn/ui-style copy-paste components for Slint native apps.

Live docs · Live demo · crates.io · npm

56 UI components, 8 installable blocks, a theme system, and a static registry you can host yourself.

slintcn Data Table slintcn Command palette slintcn Calendar

Install

Two interchangeable CLIs install the same files from the same hosted registry — pick whichever runtime you already have.

cargo (no Node required — if you build Slint apps, you already have this):

cargo install slintcn

cd your-slint-app
slintcn init --base-color zinc
slintcn add button card input dialog
# also available as a cargo subcommand: `cargo slintcn add data-table`

npm:

cd your-slint-app
npx slintcn@latest init
npx slintcn@latest add button card input dialog

Files land in ui/slintcn/you own them. Change colors in ui/slintcn/theme/tokens.slint, tweak button.slint for your product, or host your own registry. A project initialized by either CLI works with the other — they share the slintcn.json config and lockfile.

The npm CLI carries the full adoption toolkit (diff, export, --external-tokens, --import-map, …). The cargo CLI covers the core install path — init / add / list / view — for the zero-Node case.

Why it exists

shadcn proved that developers want to own UI code, not fight a theme API. Slint 1.16 is moving to Fluent as the default — fine for consistency, weak for modern dark/glass product UI. slintcn is the missing layer: tokens + primitives you copy into your repo and customize.

Quick start

# Run the visual showcase
cd examples/showcase && cargo run

# Install components into your Slint project — cargo (no Node):
cargo install slintcn
cd your-slint-app
slintcn init --base-color zinc
slintcn add button card input dialog

# …or with npm, if you prefer:
npx slintcn@latest init
npx slintcn@latest add button card input dialog

Philosophy

Phase Focus Status
v0.1 SaaS-adjacent dark glass — Button, Card, Input, Badge
v0.1.1 Foundation hardening — enum variants, focus ring, dogfooded showcase
v0.2 Shell + overlays — Label, Separator, Dialog, AlertDialog, Sheet, Tooltip, Toast
v0.3 Selection primitives + docs-style showcase — Tabs, Checkbox, Switch + Sign-in / Settings / Dashboard examples
v0.4 Select / RadioGroup / Icon + stacked Toast + runtime light/dark Theme.mode swap
v0.5 Arrow-key nav, horizontal RadioGroup, modal focus trap, build.rs scaffold hint
v0.6 PopupWindow-based Tooltip (escapes parent bounds) + font embedding guide
v0.7 Growable Rust-backed Toast queue + headless snapshot CI (SoftwareRenderer)
v0.8 PopupWindow Select (focus-based) + Toast fade-out + per-section snapshots + GitHub Actions
v0.9 shadcn fidelity pass — token recalibration (h-9, px-4, radius 10, spacing-xl/2xl), segmented Tabs, Dialog X-close + p-6 + click-absorb, all 18 primitives to measured shadcn specs
v0.10 Breadth batch A — Accordion, Avatar, Textarea, Progress, Alert, Skeleton, Toggle, ToggleGroup (26 components)
v0.11 Breadth batch B — Breadcrumb, Pagination, Table, Slider (30 components)
v0.12 Web-parity P0 — Text typography + game/HUD trio (Keycap, HudPill, SlotTile) + variant axes (Card padding/radius, Badge ghost/link, Tabs line) (34 components)
v0.13 Web-parity P1/P2 — ScrollArea (Flickable + custom scrollbar), Popover, ContextMenu (right-click) (37 components)
v0.14 Distribution backbone — registry metadata (type/title/category), CLI list/view/build, remote-URL + namespaced install, npm/HTTP-ready
v0.15 Blocks — sign-in, login, pricing, dashboard, settings as installable registry:block templates
v0.16 Theming — base-color variants (neutral/zinc/slate/stone) via slintcn init --base-color + Colors reference
v0.17 Docs IA — showcase as a docs site: install command + usage code per component section
v0.18 Docs site (ui.shadcn.com clone) — generated per-component pages with live WASM preview + install tabs + usage code + sidebar IA at /docs
v0.19 Adoption mode — install into an existing design system: external tokens, import map, filename style, dry-run/diff/no-overwrite, lockfile, export
v0.20 Adoption W2 — headless overlay panels (DialogPanel/AlertDialogPanel/SheetPanel) + external enums (--external-enums)
v0.21 Adoption W3 — docs API section (variants/sizes auto-derived) + per-item a11y/behavior contract (a11y.json, surfaced in docs + export)
v0.22 Adoption W4 (from Zero adoption testing) — unified add/diff/export pipeline (diff+export now apply external-enums), adoption flags persist to slintcn.json, per-file routes (route overlay panels out of componentsDir)
v0.22.1 Responsive + polish — mobile nav drawer for the docs, demo fills the window (no letterbox, no resize flicker), Popover/ContextMenu layout fix, Firefox first-click focus
v0.23 Blocks expansion — Team (members + roles), Profile (account form), Stats (metrics + traffic bars) → 8 blocks total
v0.24 Menu family — DropdownMenu (click), HoverCard (hover), Menubar (per-trigger popups), NavigationMenu (active-state nav) → 43 components
v0.24.1 Property docs — every in / in-out / out / callback has a one-line description; docs site surfaces them in a per-component Properties section
v0.24.2 Menu keyboard nav — Esc / ↑↓ / Enter inside DropdownMenu + Menubar popups; ←→ / Enter for NavigationMenu; a11y contracts for all four
v0.25 Command & Combobox — Combobox (searchable Select); Command (⌘K palette modal) → 45 components
v0.25.1 Combobox + Command keyboard nav — ↑/↓ move highlight, Enter selects, Esc closes (ancestor FocusScope catches keys bubbling from the search Input)
v0.26 Data Table — sortable headers + paginated rows + row clicks; consumer owns data slicing → 46 components
v0.27 Calendar + Date Picker — month grid (Calendar) + Popover-wrapped trigger (DatePicker); consumer owns date math → 48 components
v0.28 App-shellSidebar (collapsible nav with icons + active highlight), Empty (zero-state surface), AspectRatio (layout helper) → 51 components
v0.29 Catalog round-outCollapsible (single show/hide section), ButtonGroup (joined Buttons) → 53 components
v0.30 Game HUD expansionHotbar (SlotTile strip), Reticle (crosshair overlay), CompassStrip (scrolling heading) → 56 components
v0.31 MCP serverslintcn-mcp bin exposes the registry to MCP-capable AI agents (Claude Desktop, Cursor, Windsurf). Tools: list_components, list_blocks, view_component, install_command
v0.32 Directory page/docs/directory lists community + official registries; entries land via PR to registry/directory.json (ecosystem discovery surface)
v0.33 Discovery + completion — per-component snapshot pipeline (64 PNGs via --previews), /create preset page (pick components → copy install command), gallery + README hero refresh
v0.34 cargo-native installercargo install slintcn (+ cargo slintcn subcommand) installs from the hosted registry with no Node; base-color palette variants inlined into the registry so any client can pick them

SaaS-first is a wedge, not a ceiling. Once tokens + motion + hover semantics exist, a second registry (registry/game/) is just more .slint files.

Prerequisites

  • Rust with Slint 1.16 (for the showcase or your Slint app)
  • A CLI runtime — either cargo (via cargo install slintcn, no Node) or Node 20+ (via npx slintcn). You almost certainly already have cargo.
import { Button, ButtonVariant, ButtonSize } from "slintcn/components/button.slint";
import { Dialog } from "slintcn/components/dialog.slint";

Button {
    variant: ButtonVariant.glow;
    size: ButtonSize.lg;
    text: "Ship it";
    clicked => { my-dialog.open = true; }
}

// Modal must be the LAST child of Window:
my-dialog := Dialog {
    width: parent.width;
    height: parent.height;
    title: "Confirm";
    description: "Ship to production?";
}

Variants and sizes are typed enums — a typo fails to compile rather than silently falling through to the default styling.

CLI & distribution

slintcn isn't a library you depend on — it's a distribution system for copy-paste Slint code (the shadcn model). The CLI ships two ways — slintcn (after cargo install slintcn) or npx slintcn — over the same registry:

slintcn init                       # create slintcn.json + install theme tokens
slintcn add button card input      # copy components + their deps (import-rewritten)
slintcn list                       # browse the catalog, grouped by category
slintcn view dialog [--files]      # an item's metadata, install order, and source
slintcn build -o dist/registry     # emit a static registry (registry.json + r/*.json)

The cargo CLI (cargo install slintcn) covers the core path — init / add / list / view, no Node required. The npm CLI (npx slintcn) adds the full adoption toolkit (diff, export, build, --external-tokens, --import-map, …) shown below.

Config — slintcn.json (created by init): style (which registry), baseColor (neutral · zinc · slate · stone — pick at init with slintcn init --base-color zinc), outDir/themeDir/componentsDir/blocksDir (where files land — fully relocatable, imports are rewritten to match), and registries (namespace → URL).

Remote / custom registries. slintcn build emits a static registry — a registry.json index plus r/<name>.json files with each component's source inlined. Host that anywhere and install from it:

slintcn add https://zero-sq.github.io/slintcn/r/button.json   # direct URL
slintcn add @acme/button                                           # via registries config

registryDependencies (e.g. a component needing theme) resolve recursively against the same registry. The official registry is served at https://zero-sq.github.io/slintcn/r/.

Maintainers: publish the npm package with npm login && npm publish (ships bin, registry, templates, schema; prepublishOnly runs tests) and the cargo CLI with cd cli && cargo publish. Keep both versions in lockstep with registry/default/registry.json.

Community registries

slintcn isn't tied to one registry. Any host that serves a registry.json + per-item r/<name>.json (the shape slintcn build emits) is a valid registry, and slintcn add @namespace/name installs from it once you wire the namespace into slintcn.json's registries map.

Browse + list yours at zero-sq.github.io/slintcn/docs/directory — open a PR to registry/directory.json to be listed.

MCP server (AI agents)

slintcn ships a Model Context Protocol server so MCP-capable agents (Claude Desktop, Cursor, Windsurf, etc.) can browse the registry and emit the right install command without scraping docs. Zero runtime deps — pure stdio JSON-RPC.

Wire it into your client's MCP config:

{
  "mcpServers": {
    "slintcn": { "command": "npx", "args": ["-y", "-p", "slintcn", "slintcn-mcp"] }
  }
}

(slintcn-mcp is a bin entry inside the slintcn package, not a separate package — -p slintcn tells npx which package to pull, then runs that bin.)

Exposed tools:

Tool What it does
list_components Every UI component with title / category / one-liner
list_blocks Every installable block (full-screen compositions)
view_component Full metadata + usage snippet for one item
install_command Exact npx slintcn@latest add … for N items

The server reads the bundled registry/default/registry.json + the same usage snippets the docs site renders, so what it tells the agent and what slintcn add actually installs always match.

Adopting into an existing design system

The default slintcn add creates a standalone ui/slintcn island with its own Tokens. If you already have a design system — generated tokens, your own overlay root, snake_case filenames — slintcn can install into it instead of beside it. Every flag below is opt-in (set on the CLI or in slintcn.json); absent, behavior is unchanged.

# Point components at YOUR Tokens (theme not installed); land them in your dir;
# rename to snake_case; preview the plan before touching disk.
slintcn add button card \
  --external-tokens ./generated/native/tokens.slint \
  --components-dir apps/desktop/ui/primitives \
  --filename-style snake \
  --dry-run
Flag / slintcn.json key Effect
--external-tokens <path> / externalTokens Don't install the theme; rewrite every import { Tokens } from "../theme/tokens.slint" to <path>. Your file must export a Tokens global with the roles components read.
--external-enums <dir> / externalEnums Strip each component's local export enums and import (+ re-export) them from <dir>/<name>.slint — for consumers whose enums are generated elsewhere.
--components-dir <dir> / componentsDir (also themeDir/blocksDir) Where files land — fully relocatable; imports are rewritten to match.
--filename-style snake / fileNameStyle kebab (default) or snakeslot-tile.slint → slot_tile.slint, and sibling/cross-file imports follow.
--import-map <file.json> / importMap Arbitrary { "<import>": "<target>" } overrides, highest precedence.
routes (slintcn.json) Per-file destination overrides — { "components/dialog-panel.slint": "ui/surfaces/overlays/dialog_panel.slint" } — to send e.g. overlay panels somewhere other than componentsDir while their deps still import from componentsDir.
--dry-run Print the plan (+ new · ~ overwrite · = skip) and write nothing (won't even create slintcn.json).
--no-overwrite / overwrite: false Skip files that already exist.

Diff against upstream — reference, not just install. Every real add records slintcn.lock.json (a sha256 per written file). Then:

slintcn diff button     # unified diff: installed (rewritten for your config) vs registry
                        # annotates "locally modified since install" vs "upstream changed"

Headless overlay panels. If you have your own modal root, install the panel without the backdrop/mount machinery: dialog-panel (DialogPanel), alert-dialog-panel (AlertDialogPanel), sheet-panel (SheetPanel). The batteries-included Dialog/AlertDialog/Sheet are these panels composed with a Scrim + full-window mount.

Feed your own codegen. export emits one resolved item as JSON — metadata plus file content already rewritten for your config — so a monorepo codegen step can consume slintcn deterministically without a build.rs island:

slintcn export button --stdout        # JSON to stdout (pipeable)
slintcn export button                 # → dist/export/button.json

add, diff, and export all run the same resolution pipeline (import rewrite + external tokens + external enums + filename style + routes), so a diff/export reflects exactly what add writes. A first add with adoption flags also persists them into slintcn.json so follow-up commands match.

Components (default registry)

56 components across 11 categories — actions, form, layout, display, feedback, overlay, navigation, data, media, typography, hud — plus 8 installable blocks. Every component has a live preview, an auto-generated Properties section, and an accessibility contract at zero-sq.github.io/slintcn/docs — that's the canonical catalog and never drifts from the source.

A quick map of the categories:

Category What's in it
actions Button, ButtonGroup
form Input, Textarea, Label, Checkbox, Switch, RadioGroup, Select, Combobox, Toggle, ToggleGroup, Slider, Calendar, DatePicker
layout Card, Separator, AspectRatio
display Badge, Accordion, Collapsible, Empty
feedback Alert, Progress, Skeleton, Toast
overlay Dialog (+ Panel), AlertDialog (+ Panel), Sheet (+ Panel), Tooltip, Popover, ContextMenu, DropdownMenu, HoverCard, Menubar, Command
navigation Tabs, Breadcrumb, Pagination, NavigationMenu, Sidebar
data Table, DataTable
media Avatar, Icon
typography Text
hud Keycap, HudPill, SlotTile, Hotbar, Reticle, CompassStrip

Blocks: Sign-in, Login, Pricing, Dashboard, Settings, Team, Profile, Stats.

Keyboard contracts (verified per component in a11y.json)

  • Buttons / Checkbox / Switch / Toggle — Enter / Space activates.
  • Tabs / RadioGroup / Slider — Arrow keys navigate.
  • Select / DropdownMenu / Menubar / Combobox / Command — Arrow keys + Enter inside an open popup; Escape closes.
  • Dialog / AlertDialog — Tab trapped inside; Escape dismisses.
  • NavigationMenu — ←/→ moves the active item, Enter fires navigate.

Composed examples

cd examples/showcase && cargo run opens a sidebar-navigated app exercising every primitive. The eight blocks ship as standalone screens: Sign-in, Login, Pricing, Dashboard, Settings (auth + product UI) and Team, Profile, Stats (members management, account form, analytics).

Mounting overlays

Slint has no portal primitive, so modals mount as the last child of Window and span the full window area:

Window {
    VerticalLayout { /* main UI */ }

    Dialog {
        width: parent.width;
        height: parent.height;
        open <=> dialog-state;
        title: "…";
    }

    Toaster { width: parent.width; height: parent.height; }
}

The Scrim in popup-helpers.slint only mounts its TouchArea while shown, so closed modals don't block interaction with the underlying UI.

vs alternatives

std-widgets / Fluent slint-ui-system slintcn
Model Framework widgets Crate dependency Copy-paste
Aesthetic Platform / Fluent Neon dashboard Dark glass / shadcn-like
Customize Theme API Crate version lock Edit the .slint file
Overlays Limited PopupWindow Dialog · Sheet · Tooltip · Toast

Project layout

registry/default/         # Source of truth (published with npm package)
  theme/palette.slint     #   raw color/alpha primitives
  theme/tokens.slint      #   semantic layer (components read this)
  components/*.slint      #   56 components + popup-helpers + lucide-paths
examples/showcase/        # Runnable gallery (regenerated via `slintcn add`)
bin/slintcn.mjs           # init + add CLI (transitive deps)
bin/__test__/             # node:test suite — `make test`

Run make verify before committing — it runs node tests, cargo build, and cargo clippy -D warnings end-to-end. Run make snapshot to headlessly render each showcase section to docs/img/snapshots/section-<n>-<name>.png via Slint's SoftwareRenderer (no display server required); per-section baselines live in the repo for visual-regression diffs.

Quality gate is local (no CI workflow): a tracked pre-push hook runs make verify (node tests + cargo build + clippy -D warnings) and blocks the push on failure. Enable it once per clone with git config core.hooksPath .githooks; bypass a single push with git push --no-verify. Run make snapshot to refresh the visual-regression baselines.

Toast Rust glue (required for Toaster to function)

Toast's queue lives in Rust (slint::VecModel<ToastItem>) — pure-Slint array mutation is too limited in 1.16 for a real stack. Mount this glue in your app's main.rs:

use slint::{ComponentHandle, Model, ModelRc, SharedString, Timer, TimerMode, VecModel};
use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration};

let items: Rc<VecModel<ToastItem>> = Rc::new(VecModel::default());
let next_id = Rc::new(RefCell::new(1i32));
let timers: Rc<RefCell<HashMap<i32, Timer>>> = Rc::default();
let queue = ui.global::<ToastQueue>();
queue.set_items(ModelRc::from(items.clone()));
// see examples/showcase/src/main.rs for the full on_show / on_dismiss closures

Required Slint feature: slint = { version = "1.16", features = ["compat-1-2"] }. For visual-regression snapshots also enable "renderer-software".

Roadmap

See docs/ROADMAP.md.

License

MIT