mcpField
M
marko-ui

marko-ui

github.com/svalloryDevelopment5131 installsUpdated 4w ago
Repository

About marko-ui

marko-ui is a CLI skill for using Marko 6 UI components from a registry. It guides component discovery, documentation lookup, installation, and change checks.

Key features

  1. 01Search the component registry
  2. 02Read component documentation
  3. 03Add and compare components
  4. 04Check project configuration
  5. 05Sync component lists for agents

Use cases

  1. 01Find a suitable Marko 6 component
  2. 02Review props before writing markup
  3. 03Install or update UI components
  4. 04Check local changes against registry

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/svallory/marko-ui --skill marko-ui

After installing, type/marko-uiin Claude Code to confirm it loaded.

Details

From the project README

marko-ui

marko-ui is shadcn/ui for Marko 6: accessible components whose behavior comes from Zag.js state machines and whose styling is Tailwind v4. Use the marko-ui CLI for everything below — do not hand-write a component that the registry already ships, and do not guess a component's props.

Run the CLI with the project's package runner (bunx marko-ui, npx marko-ui, pnpm dlx marko-ui). Every command is non-interactive when stdin is not a TTY: confirmations are taken as yes, init uses its defaults, and add with no component names exits 2 instead of opening a picker.

First: which distribution is this project on?

Read distribution in components.json (absent means copy). It changes how components are obtained and customized.

copy (default)import
Components live inthe project, under the ui alias (src/components/ui/<name>/)node_modules/@marko-ui/shadcn
Get a componentmarko-ui add <name> -ynothing to add — import @marko-ui/shadcn/ui/<name>/<name>.marko
Customizeedit the component file (it is the project's code)override mu-* hook classes from the project stylesheet

No components.json means marko-ui is not set up: run marko-ui init (add --agents to also write the agent docs).

Workflow

  1. marko-ui search -q <query> — find a component (--json for machine output). It matches whole words against each item's name, title and description; each extra word narrows the ranking, and items matching none of your words are left out. Every item costs context, so search before guessing a name. The installed ones are listed in the project's AGENTS.md.
  2. marko-ui docs <name> — the component's docs as markdown: install, usage, parts, props, events, keyboard/accessibility, then the essential examples. Read this before writing markup for a component you have not used in this session. It is deliberately small — at most three examples (the first plus the ones flagged essential), with the rest listed under "More examples". --examples prints every example; --example <id…> prints only the ones you name. --list prints the index. docs <name> --json returns the same markdown (and the structured model it was rendered from) in the envelope below. The markdown is written for you: demo sources come through with maintainer comments stripped, the prop list is one line per prop (name: type = default — description), a = fixed X cell means the component sets that value and yours is ignored, and | undefined is dropped since almost everything is optional (required props are marked); an absent default means not recorded, NOT "there is no default". Import paths in the snippets follow the project's own components.json (distribution + aliases.ui), so what you read is what lands in the project. For a component the project has INSTALLED, docs describes the installed version without the network — the copy distribution reads the copy add fetched and cached under node_modules/.cache/marko-ui/ (an add that could not fetch a docs file still installs, with a DOCS_CACHE_FAILED warning, and docs reads the registry later), the import distribution reads the docs inside the installed @marko-ui/shadcn — and omits the install line; stderr then says source: installed (…) (data.components[].source under --json). --remote is the way out of the installed version: it reads the registry's current docs (what an upgrade would bring) even for an installed component. The registry's docs are one file per component that the item references, so show <name> prints a componentDocsRef URL, never the docs themselves; a registry item without one has no docs, and docs says so (NOT_FOUND, suggesting show). A component that is not installed comes from the registry, with its exact marko-ui add <name> -y.
  3. marko-ui show <name> — the registry item as JSON; --files lists what would be written, --deps lists npm and registry dependencies. show is a JSON command in EVERY mode, so --json is accepted and changes nothing, and a show failure is the marko-ui/error envelope on stdout even in a terminal — the same document a pipe would get, not prose. Parse stdout either way.
  4. marko-ui add <name> -y --json — install (copy distribution). Always pass --json when you are driving this yourself: it returns which files were created / updated / skipped, which dependencies were installed, and any warnings, so you never have to parse the human output to find out what happened. --dry-run previews the same shape with dryRun: true and PLANNED statuses (--dry-run --json); --overwrite replaces local files.
  5. marko-ui diff <name> --json — compare local edits against the registry version: every file with a status (unchanged / modified / missing) and, for a modified one, the unified diff as plain text with no ANSI. ok is true whether or not anything differs — diff reports, it does not judge.
  6. marko-ui doctor --json — health checks; exit code 3 means something is broken and each failed check carries a fix field with the command (or one-line action) that fixes it. Run it after init/add and before reporting the work done.
  7. marko-ui agents sync --json — refresh the component list in AGENTS.md after adding or removing components. agentsChanged says whether the file actually changed; skills says which agent skills were installed vs already present.

marko-ui manifest prints every command, flag, exit code, and error code as JSON when you need the exact surface; marko-ui manifest <command> prints just that one command (name or alias) plus the exit codes — cheaper when you are checking a single flag. An unknown name is a usage error (exit 2).

Reading JSON

Every --json payload is one document: { "$type", "version", "ok", "data" }. Branch on $type before reading data — $type is marko-ui/search, marko-ui/status, marko-ui/show, marko-ui/docs, marko-ui/docs.list, marko-ui/doctor, marko-ui/registry.list or marko-ui/manifest. It is printed MINIFIED on one line (a pipe or a program) and pretty-printed only when stdout is a terminal, because the reader of this output is usually a program and indentation is tokens you pay for. There is no --pretty flag.

search items carry name, short type ("ui", "block"), description and registry — nothing derivable. To install a result from a registry other than the default, pass <registry>/<name>; addArgument in data states that rule once, and only when a non-default registry is in the results.

status reports config.distribution, config.visualStyle and config.iconLibrary; config.resolvedPaths gives cwd (absolute, once) and every other path RELATIVE to it.

A failure is the marko-ui/error envelope described under Reading errors — never prose. The one command where that holds in EVERY mode, terminal included, is show: it is a JSON command in every mode, so its failures are documents on stdout too.

Reading what a command changed

add, init, eject, diff and agents sync all return the same three shapes, so one parser covers all of them:

  • files: { path, status }, relative to cwd (given once at the top).
  • warnings: { code, message, fix }. A warning is NOT a failure — the command did its job, ok stays true, and the warning says what is left. Codes are listed in marko-ui manifest under warningCodes. The human path prints the SAME message and fix, so what you read and what you parse cannot disagree.
  • next: commands worth running next. Act on it rather than re-deriving it.

status values are a closed vocabulary, not prose:

statusmeans
createdthis run wrote a file that did not exist
updatedthis run replaced an existing file
unchangedthe file already matched the registry byte for byte
skippedthe file exists, differs, and was left alone (--overwrite would have replaced it)
removeda stale icon map this run deleted
missing(diff only) the registry has it, the project does not

skipped and unchanged are different facts. Re-running add on an unchanged component reports unchanged, not skipped; skipped means the file had drifted and nothing replaced it. Read the status before telling a user something was installed — created is the only status that means "this run wrote it".

For add, also check items.resolved (what was ACTUALLY installed, which includes registry dependencies pulled in beyond what you asked for — add button can resolve utils too), dependencies (installed / present / failed), and dryRun: true on a preview, where the statuses are PLANNED, not observed.

Reading errors

stdout carries the result; stderr carries every problem. If you run the CLI with a pipe or capture stdout, you get the command's payload and nothing else — no log line, no warning, no failure text. Never parse stderr as data.

With --json, a failure is one JSON object on stdout and nothing else, in this shape:

{
  "$type": "marko-ui/error",
  "version": 1,
  "ok": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Registry item \"buton\" was not found. Did you mean \"button\"?",
    "suggestion": "Did you mean \"button\"?",
    "details": { "itemName": "buton", "suggestions": ["button"] }
  }
}

Branch on error.code, never on the message text. Every code is listed in marko-ui manifest under errorCodes — the common ones are NOT_FOUND (unknown item), NOT_CONFIGURED (no components.json, or a registry that isn't configured), INVALID_CONFIG, NETWORK_ERROR (registry/docs host unreachable — exit 4), FETCH_ERROR (the host answered 5xx or 429 — exit 4, retry later), MISSING_ENV_VARS, MONOREPO_ROOT (run from a workspace, not the repo root), USAGE_ERROR (bad invocation, including an unknown command or flag), CHECK_FAILED (a doctor/validate/agents --check found problems), and PROJECT_NOT_FOUND (no Marko project at that directory).

A connection failure (refused port, DNS failure, timeout) is NETWORK_ERROR with exit 4 on every registry-backed command. A server that answered 5xx or 429 is FETCH_ERROR, also exit 4: the registry is failing and the same command may succeed later. Every other HTTP status exits 1 with its own code: UNAUTHORIZED (401), FORBIDDEN (403), NOT_FOUND (404), GONE (410), REQUEST_REJECTED for an unexpected 4xx such as 400 — retrying will not change them.

details.suggestions carries "did you mean" candidates when the registry index was reachable, so you can retry without parsing prose. An unknown item name in docs, show, add or diff is the common case.

details never carries a stack trace, an absolute path under your home directory, or a credential-bearing URL — paths are shortened and registry URLs have their query string and credentials stripped. A stack is on stderr only under MARKO_UI_DEBUG.

One name can fail while the others succeed: marko-ui docs nope button prints button's markdown on stdout, reports nope on stderr, and exits 1.

Exit codes: 0 ok · 1 operational failure · 2 usage error (the code USAGE_ERROR, always and only — a wrong or missing argument or flag value; a missing project, an uninitialized project or the wrong distribution are their own codes, exit 1) · 3 a check found problems · 4 the registry could not be reached or is failing (connection failure, 5xx, 429). marko-ui manifest lists both tables (exitCodes, errorCodes).

Using components

import Dialog from "../components/ui/dialog/dialog.marko";
import Button from "../components/ui/button/button.marko";

<let/open=false/>

<Dialog open:=open>
  <@trigger|props|>
    <Button ...props>Edit profile</Button>
  </@trigger>
  <@title>Edit profile</@title>
  <@description>Make changes to your profile here.</@description>
  <p>Form fields go here.</p>
</Dialog>
  • Parts are attribute tags (<@trigger>, <@title>), not separately imported subcomponents. marko-ui docs <name> lists each component's parts.
  • <@trigger|props|> hands you the machine props to spread onto whatever element you render. It is the equivalent of Radix's asChild.
  • State binds with := (open:=open, value:=value, checked:=checked), or pass the value plus its xChange handler (open=open openChange=(v) => { open = v }).
  • On the copy distribution add also generates a taglib (marko.json), unless the project already has its own marko.json, so installed components are usable as tags without an import (<Button>, <CardHeader>). An explicit import always works too.
  • Never import a component as Input. It shadows Marko's reserved props type. The input component is imported as TextInput.

Pitfalls that break components silently

Each of these has caused a real bug. Check your code against the list before finishing.

  1. Controlled props pin the machine. open= / value= / checked= WITHOUT the matching openChange / valueChange / checkedChange (or the := bind) freezes the component in that state. For an initial value only, use defaultOpen= / defaultValue= / defaultChecked=.
  2. Boolean aria-* / data-* attributes serialize wrong. Marko renders boolean true as a bare attribute: aria-selected=(bool) emits aria-selected="" and data-active=(bool) never matches a data-[active=true]: Tailwind variant. Wrap in String(...).
  3. Nothing from Zag crosses a tag boundary or lives in state. Machine services and api objects contain functions and cannot be serialized for resume. Pass plain data, or a closure written in the template (parentApi=() => api()), and call api() at the use site.
  4. Do not read a parent component's api() inside a child's attribute-tag body. Route it through the child's input as a closure instead. The symptom is TypeError: ... is not a function after an overlay opens.
  5. Overrides that do nothing (import distribution). The style layer must be imported with layer(components) and the matching @custom-variant style-<name> line must exist in the project's stylesheet. Both fail silently.
  6. Unstyled components (copy distribution). The theme stylesheet is not loading. Check that the file recorded as tailwind.css in components.json is imported by the root layout and that @tailwindcss/vite is in the Vite config. marko-ui init wires both; a hand-rolled setup often misses one.
  7. "Cannot find module @zag-js/..." or marko-zag. A component's npm dependencies are missing. marko-ui add installs them; marko-ui show <name> --deps lists them.

For Marko 6 language rules (<let>, <const>, <for>, event handlers, TypeScript in templates) use the marko6 skill; for @marko/run routing use marko-run.

Editing a component (copy distribution)

The file is project code. Each .marko file starts with a static const styles = { ... } block holding the Tailwind classes per part: change classes there. Interactive components wire their machine with one tag, <zag/api=() => machine from=input/>, which picks the machine's props out of input and returns the api getter; keyboard handling, focus management, and ARIA come from the machine, so restyling cannot break accessibility. After editing, marko-ui diff <name> shows what diverged from the registry.

More

Comments

Markdown supported