element
Publishing your own elements
Put UI source in a public GitHub repo, one folder per element, with a small manifest that declares files and dependencies. Anyone can then install it with npx @levi-putna/element. The CLI resolves the dependency tree, installs missing npm packages, copies source, and writes sidecars so updates stay trackable.
Installing from someone else's catalog? See Using.
Checklist
From empty repo to installable catalog
- 1
Create a public GitHub repo
Any public repo works. No npm package or private registry required.
- 2
Add a catalog folder
Put each element in registry/<name>/ or elements/<name>/.
- 3
Add source + a .element manifest
List files to copy, npm packages to install, and other catalog elements this one depends on.
- 4
Commit and push
Push to your default branch (main unless you document otherwise).
- 5
Test the install
Run list and add against your username/repo from a scratch project.
- 6
Share the install command
Give users: npx @levi-putna/element@latest add your-username/your-repo
How the tool manages it
Catalog → dependency tree → plan → write
When someone runs add, the CLI does not copy a single folder blindly. It builds a tree from your manifests, unions npm deps across that tree, shows a plan, then writes everything in order.
Detect catalog
Looks for registry/ or elements/ on GitHub
Read manifests
Prefers .element, then registry.json
Resolve element deps
Walks elementDependencies recursively
npm packages
Union deps; skip ones already in package.json
Source files
Download, plan +/~/=, write + sidecar + lock
After install, the consumer project has .element/lock.json (provenance for update/remove) and a <name>.element.json sidecar next to each installed element (paths, deps, source). That is how later installs know where elements already live.
Layout
One folder per element
The catalog root must be named registry/ or elements/. Each child directory is one installable element; the folder name is what consumers pass to --element.
your-elements-repo/
registry/ # or elements/ (CLI looks for either)
utils/
.element
utils.ts
badge/
.element
badge.tsx
card/
.element # depends on utils + badge
card.tsx
README.md # optional human docsIf a folder has no .element and no registry.json, the CLI invents a minimal manifest from the source files in that folder (everything except README/markdown). Prefer an explicit manifest so you control deps and file targets.
Create an element
Source files + a .element manifest
Minimal publishable element: one component file and a .element JSON file beside it. The files list is what actually gets copied; target is only the filename under the consumer's chosen install path (full paths like components/ui/button.tsx are reduced to button.tsx).
mkdir my-elements && cd my-elements
git init
mkdir -p registry/hello-button
cat > registry/hello-button/.element << 'EOF'
{
"name": "hello-button",
"type": "ui",
"description": "A minimal button element for testing the element CLI.",
"dependencies": [],
"devDependencies": [],
"elementDependencies": [],
"files": [
{ "path": "hello-button.tsx", "target": "hello-button.tsx" }
],
"category": "ui"
}
EOF
cat > registry/hello-button/hello-button.tsx << 'EOF'
export function HelloButton({ children = "Hello" }: { children?: React.ReactNode }) {
return (
<button type="button" className="rounded-md bg-black px-3 py-2 text-sm text-white">
{children}
</button>
)
}
EOF
git add . && git commit -m "Add hello-button element"
gh repo create my-elements --public --source=. --pushDependencies
Two kinds, both managed by the CLI
An element can depend on npm packages and on other elements in the same catalog. Declare both in the manifest; the CLI handles install order and missing packages so consumers do not have to.
| Kind | Manifest field | How the CLI manages it |
|---|---|---|
| npm dependencies | dependencies / devDependencies | Unioned across the whole install tree. Already-declared packages in package.json are skipped. Installed with the detected package manager (yarn preferred when ambiguous). |
| Element dependencies | elementDependencies / registryDependencies | Resolved recursively in the same catalog before the requested element. Marked as (dependency) in the install plan. Missing names are skipped with a warning. |
Example: card depends on utils + badge
Ship utils and badge as their own folders. card lists them under elementDependencies. Installing card alone pulls all three.
{
"name": "utils",
"type": "lib",
"description": "Shared cn() helper.",
"dependencies": ["clsx", "tailwind-merge"],
"devDependencies": [],
"elementDependencies": [],
"files": [
{ "path": "utils.ts", "target": "utils.ts" }
],
"category": "lib"
}{
"name": "card",
"type": "ui",
"description": "A simple card that uses badge and shared utils.",
"dependencies": ["class-variance-authority"],
"devDependencies": [],
"elementDependencies": ["utils", "badge"],
"files": [
{ "path": "card.tsx", "target": "card.tsx" }
],
"category": "ui"
}# Consumer runs:
npx @levi-putna/element@latest add you/my-elements --element card
# CLI resolves:
# utils (dependency) ← from card.elementDependencies
# badge (dependency) ← from card.elementDependencies
# card (requested)
#
# Then unions npm packages from all three manifests,
# shows a plan, and on confirm writes every file + sidecar.Resolution details from the CLI: dependencies are walked depth-first before the requested element is recorded, so files land in dependency order. Namespaced refs like @shadcn/button resolve to the last path segment (button) in the same catalog. URL dependency refs are kept but not fetched recursively in the current version. Official shadcn-style names that are not in your catalog are listed under Skipped (not in catalog) in the plan.
Manifests
Preferred: .element
Keep the manifest next to the source files. Prefer .element for new catalogs. If both .element and registry.json exist, .element wins.
{
"name": "scheme",
"type": "ui",
"description": "Strata scheme identity primitives.",
"dependencies": ["lucide-react"],
"devDependencies": [],
"elementDependencies": ["utils", "badge"],
"files": [
{ "path": "scheme.tsx", "target": "scheme.tsx" }
],
"category": "ui"
}Compatible: shadcn registry.json
Existing shadcn registries work without changes. registryDependencies is treated the same as elementDependencies. The CLI uses the filename from target and installs into the path the consumer chooses.
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "button",
"type": "registry:ui",
"description": "Triggers an action or event.",
"dependencies": ["lucide-react"],
"registryDependencies": ["utils"],
"files": [
{
"path": "registry/button/button.tsx",
"target": "components/ui/button.tsx",
"type": "registry:ui"
}
]
}Manifest fields
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | - | Element identifier. Should match the folder name (registry/card/ → card). |
description | string | - | Shown in list and the interactive install picker. |
dependencies | string[] | [] | npm packages the CLI installs into the consumer project if missing (e.g. lucide-react, class-variance-authority). |
devDependencies | string[] | [] | npm packages installed as devDependencies if missing. |
elementDependencies | string[] | [] | Other elements in the same catalog to install first. Use registryDependencies in shadcn manifests; both are merged. |
files | array | - | Source files to copy. path is relative to the element folder (or a repo path); target is the filename written into the consumer install path. |
type / category | string | ui | Optional labels for grouping. registry: prefixes are stripped. |
npm packages
Declared once, installed only if missing
List every runtime package your source imports under dependencies. Use package@version when you need a specific version; bare names are skipped if already present in the consumer's package.json.
The CLI detects yarn, pnpm, bun, or npm from lockfiles and packageManager, and prefers yarn when there is no signal. Packages from the whole dependency tree are unioned once, then installed before any files are written.
The consumer project must already have a package.json. Element does not create one.
Verify
Test before you share
Install into a throwaway project from the published branch. Confirm the dependency elements, npm packages, file list, sidecar, and lock entry.
# 1. Confirm the CLI can see your catalog
npx @levi-putna/element@latest list your-username/your-repo
# 2. Install into a scratch project
mkdir /tmp/element-test && cd /tmp/element-test
yarn init -y
npx @levi-putna/element@latest add your-username/your-repo --element card --path components/ui
# 3. Verify files, deps, and sidecars
ls components/ui/
cat components/ui/card.element.json
cat .element/lock.json
# 4. After you push a change, re-fetch
npx @levi-putna/element@latest update cardTroubleshooting
Common publish issues
| Problem | Likely cause | Fix |
|---|---|---|
| No elements found | Repo is private, wrong branch, or missing registry/ / elements/ | Make the repo public, confirm the branch, check folder names |
| Element "foo" not found | Folder name mismatch | Folder must be registry/foo/ (or elements/foo/) and users pass --element foo |
| Install plan has no files | Missing or empty files list, and no source files in the folder | Add files with path + target in .element, or put source files beside the manifest |
| Skipped (not in catalog) | elementDependencies names a folder that does not exist | Ship that element in the same catalog, rename the dependency, or remove it |
| No package.json found | Consumer project is not a Node project yet | Run yarn init / pnpm init / npm init before add |
| Rate limit errors | Unauthenticated GitHub API limits | Set GITHUB_TOKEN in the environment |
Installing from a catalog instead? Using element →