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. 1

    Create a public GitHub repo

    Any public repo works. No npm package or private registry required.

  2. 2

    Add a catalog folder

    Put each element in registry/<name>/ or elements/<name>/.

  3. 3

    Add source + a .element manifest

    List files to copy, npm packages to install, and other catalog elements this one depends on.

  4. 4

    Commit and push

    Push to your default branch (main unless you document otherwise).

  5. 5

    Test the install

    Run list and add against your username/repo from a scratch project.

  6. 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.

text
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 docs

If 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).

sh
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=. --push

Dependencies

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.

KindManifest fieldHow the CLI manages it
npm dependenciesdependencies / devDependenciesUnioned across the whole install tree. Already-declared packages in package.json are skipped. Installed with the detected package manager (yarn preferred when ambiguous).
Element dependencieselementDependencies / registryDependenciesResolved 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.

json
{
  "name": "utils",
  "type": "lib",
  "description": "Shared cn() helper.",
  "dependencies": ["clsx", "tailwind-merge"],
  "devDependencies": [],
  "elementDependencies": [],
  "files": [
    { "path": "utils.ts", "target": "utils.ts" }
  ],
  "category": "lib"
}
json
{
  "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"
}
sh
# 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.

json
{
  "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.

json
{
  "$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

PropTypeDefaultDescription
namestring-Element identifier. Should match the folder name (registry/card/ → card).
descriptionstring-Shown in list and the interactive install picker.
dependenciesstring[][]npm packages the CLI installs into the consumer project if missing (e.g. lucide-react, class-variance-authority).
devDependenciesstring[][]npm packages installed as devDependencies if missing.
elementDependenciesstring[][]Other elements in the same catalog to install first. Use registryDependencies in shadcn manifests; both are merged.
filesarray-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 / categorystringuiOptional 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.

sh
# 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 card

Share

Give consumers one command

Interactive install for browsing, or a named element for docs and READMEs. Same pattern as the install blocks on this site's Elements pages.

sh
npx @levi-putna/element@latest add your-username/your-repo
npx @levi-putna/element@latest add your-username/your-repo --element card

Example catalog: levi-putna/elements. CLI source: levi-putna/element.

Troubleshooting

Common publish issues

ProblemLikely causeFix
No elements foundRepo is private, wrong branch, or missing registry/ / elements/Make the repo public, confirm the branch, check folder names
Element "foo" not foundFolder name mismatchFolder must be registry/foo/ (or elements/foo/) and users pass --element foo
Install plan has no filesMissing or empty files list, and no source files in the folderAdd files with path + target in .element, or put source files beside the manifest
Skipped (not in catalog)elementDependencies names a folder that does not existShip that element in the same catalog, rename the dependency, or remove it
No package.json foundConsumer project is not a Node project yetRun yarn init / pnpm init / npm init before add
Rate limit errorsUnauthenticated GitHub API limitsSet GITHUB_TOKEN in the environment

Installing from a catalog instead? Using element →