JLDS is a design system and component CLI. Instead of installing components as a package dependency, jlds downloads component source code directly into your project — you own it, read it, and customize it freely.
The aesthetic: a premium SaaS product, light-first — a low-glare off-white canvas with a deep-emerald accent. Clean layouts, strong hierarchy, soft 12–16px corners, comfortable spacing, subtle depth (hairline borders over heavy shadows), and small, confident micro-interactions. A full dark theme ships alongside, opt-in via <html data-theme="dark">.
Supported frameworks: React, Vue
Styling: Self-contained CSS (.jl-btn-style classes) + CSS variable design tokens — Tailwind is not required
Docs: Full documentation lives in /docs (VitePress) — run pnpm --dir docs run docs:dev for a local preview.
No Rust, no global install — run it on the fly with npx:
npx @jarooda/jlds init
npx @jarooda/jlds add buttonOr install globally (npm i -g @jarooda/jlds, then use the jlds command), or from source for Rust users (cargo install --git https://github.com/jarooda/jlds.git jlds). The npm package is a thin launcher around the native Rust binary — same CLI either way. See Getting Started.
Building the CLI from source for development? See Contributing / CLI development below.
jlds add button
This fetches button.tsx + button.css (or Button.vue) from the registry and writes them into your project under the path configured in jlds.json. The files are self-contained — no runtime dependency on JLDS, no Tailwind required.
jlds/
├── cli/ # Rust CLI — the jlds binary
├── registry/ # Component source of truth (served via jsDelivr)
│ ├── registry.json
│ ├── css/ # single source of truth for component CSS
│ │ ├── index.css # design tokens + base resets (also used by `jlds init`)
│ │ └── button.css # .jl-btn class system
│ └── components/
│ └── button/
│ ├── meta.json
│ ├── react/
│ │ ├── button.tsx
│ │ └── index.ts
│ └── vue/
│ ├── Button.vue
│ └── index.ts
├── demo/
│ ├── react/ # Vite + React 19 demo project
│ └── vue/ # Vite + Vue demo project
├── todo/
│ ├── TODO.md
│ └── deploy-to-registry.md
└── README.md
- Rust (1.80+)
- Node.js + a package manager (npm, pnpm, yarn, or bun) — for demo projects
cd cli
cargo build # debug build
cargo install --path . # install jlds globallyjlds --helpWhenever you change the CLI source, re-run
cargo install --path .to update the globally installedjldsbinary — otherwise your shell keeps using the old build.
The repo ships a tracked pre-commit hook in .githooks/ that regenerates derived,
committed artifacts (currently the registry/css/all.css bundle) and
stages them, so they never drift from their sources. Enable it once per clone:
git config core.hooksPath .githooksThe hook only touches committed artifacts — generated-but-gitignored output (docs public/css,
VitePress dist) is rebuilt by the docs scripts and never committed, so it's deliberately left
out.
The registry is just a folder of static files (registry/). For local development, point jlds.json directly at that folder on disk — no server required:
{
"registry": "../../registry"
}The path is resolved relative to the directory jlds is run from (e.g. demo/vue/ or demo/react/). Any path starting with /, ./, ../, or file:// is treated as local.
For production, the registry is deployed to GitHub + jsDelivr. See todo/deploy-to-registry.md for the full process. Once deployed, jlds.json points at:
{
"registry": "https://cdn.jsdelivr.net/gh/<org>/jlds@main/registry"
}cd demo/react # or demo/vue
jlds initjlds init auto-detects everything from package.json:
- Framework — React (or Next.js) / Vue (or Nuxt). Errors if neither or both are found.
- TypeScript — from
package.jsonor presence oftsconfig.json - Tailwind — detected and version-labeled if present, but not required
You'll only be prompted for:
- Global CSS file path (where design tokens get injected)
- Components install path (default
src/components/ui) - Utils install path (default
src/lib/utils)
jlds init then injects the JLDS design tokens (colors, typography, spacing, radius, shadows, motion + Geist font) into your global CSS file. If the file already has content, the @import is hoisted to the top and the :root token block is appended — existing styles are preserved.
For local development, point the registry at the local folder after init (see above).
jlds add buttonFiles land in the path configured under paths.components in jlds.json.
For the React demo: src/components/ui/button/
button/
├── button.css # .jl-btn class system — variants, sizes, states (from registry/css/button.css)
├── button.tsx # self-contained component
└── index.ts # re-exports Button, ButtonProps, ButtonVariant, ButtonSize
For Vue, the same button.css lands alongside Button.vue and index.ts, referenced via <style src="./button.css">.
import { Button } from "@/components/ui/button"
<Button variant="primary" size="md">Click me</Button>Available variants: primary, secondary, ghost, subtle, danger
Available sizes: sm, md, lg
If the registry component declares dependencies in meta.json, jlds add installs them automatically using your detected package manager (pnpm/yarn/bun/npm, based on lockfile).
No CLI, no build step — link the stylesheets straight from jsDelivr and use the .jl-* classes directly:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/<org>/jlds@main/registry/css/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/<org>/jlds@main/registry/css/button.css">
<button class="jl-btn jl-btn--primary jl-btn--md">Click me</button>css/index.css ships the design tokens, base resets, and Geist font import — always include it first. Each component then has its own css/<name>.css with just that component's classes.
Want everything in one link? Use the bundle, which @imports index.css plus every component:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/<org>/jlds@main/registry/css/all.css">css/all.css is generated by node registry/scripts/build-css-bundle.mjs — re-run it whenever you add or remove a component stylesheet.
registry/css/<name>.css is also the single source of truth for jlds add — the CLI fetches it directly and writes it as <name>.css alongside the component, so the React .css file, the Vue <style src="..."> file, and the vanilla CDN file are always identical.
When you update a component in registry/components/<name>/, sync it to demo projects with:
jlds update buttonThis re-fetches from the registry and overwrites the local files.
Typical workflow when editing a component:
# 1. Edit the component source in the registry
vim registry/components/button/react/button.tsx
# 2. Make sure jlds.json points at the local registry (see above)
# 3. Sync to demo
cd demo/react && jlds update button
# 4. Check the result
cat src/components/ui/button/button.tsx| Command | Description |
|---|---|
jlds init |
Auto-detect framework/TS/Tailwind, create jlds.json, inject CSS design tokens |
jlds add <name> |
Download a component into your project |
jlds update <name> |
Re-fetch a component from the registry (overwrites local) |
jlds list |
List all components available for your framework |
JLDS uses CSS variables for design tokens, and is light-first. jlds init injects the full token set into your global CSS file — a light :root default plus an opt-in dark theme:
:root {
--accent: var(--brand-600); /* deep emerald, #157053 on light */
--accent-hover: var(--brand-700); /* hover deepens on light */
--bg-app: #f3f5f3; /* low-glare off-white canvas */
--surface-card: #ffffff;
--text-primary: #161b18;
--radius-control: var(--radius-xl);
--shadow-xs: 0 1px 2px hsl(220 24% 22% / 0.06);
/* ... full ramp: neutrals, semantic colors, typography, spacing, motion */
}
/* Dark theme — opt-in by setting data-theme="dark" on <html> */
[data-theme="dark"] {
--accent: var(--brand-500);
--accent-hover: var(--brand-400); /* on dark, hover lightens instead */
--bg-app: var(--neutral-950);
--surface-card: var(--neutral-900);
--text-primary: #eaf1ed;
}JLDS does not auto-follow the OS prefers-color-scheme — theming is a deterministic attribute toggle on <html> (or any wrapper):
document.documentElement.setAttribute("data-theme", "dark"); // or remove it for lightOverride any token in your own CSS (after the injected block) to re-theme without touching component files. Components reference only the semantic aliases (--accent, --surface-card, --text-primary, …) — never raw ramps — so re-accenting is a matter of repointing --accent*:
:root {
--brand-600: #6d28d9; /* swap emerald for purple (light accent) */
}Components ship their own scoped CSS classes (.jl-btn, .jl-btn--primary, etc.) built entirely on these tokens — no Tailwind config or utility classes required.