# Introduction
> What Ndop UI is, and how it differs from a component library you install as a package.
Ndop UI is a set of accessible Angular components that you add to your app
as source files. You run `ndop add dialog`, and the dialog's code lands in
your project, ready to read, restyle or change. Nothing hides behind a
package boundary.
## How it works
Ndop has two layers:
- **Primitives** in `@ndop/primitives`: headless directives that implement
the behavior, keyboard support and ARIA of each pattern. You install them
as an npm package and update them like any dependency.
- **Styled items** from the registry: thin directives that wrap a primitive
with Tailwind classes. These are the files `ndop add` copies into your
project. They look like [shadcn/ui](https://ui.shadcn.com) and use the
same design tokens.
The CLI records the version and a hash of every file it writes in
`ndop.lock.json`. That record is what lets a later version merge upstream
fixes into files you changed, instead of overwriting them.
Coding agents get the same components through an MCP server, guidance in
your project, and `ndop verify`, which checks their work and says what to
fix.
## What it's built on
- Angular 21 or 22, standalone, signals-first and zoneless. Apps that still
use zone.js work too.
- Tailwind CSS v4, configured in CSS.
- The Angular CDK for overlays, focus management and directionality.
- Server-side rendering and hydration, tested on every component.
## Credits
Ndop's styled items and its default theme start from
[shadcn/ui](https://ui.shadcn.com): the classes of its new-york-v4 style
and its neutral palette, MIT licensed, copyright (c) 2023 shadcn. The
behavior underneath is Ndop's own, on the Angular CDK. The registry
serves its [license and third-party notices](/r/LICENSE.txt) with the
items.
## Next steps
- [Install Ndop](/docs/getting-started/installation) in an Angular app.
- Learn how [theming](/docs/getting-started/theming) and
[dark mode](/docs/getting-started/dark-mode) work.
- See what the [CLI](/docs/getting-started/cli) can do.
- Set up your coding agent with [Agents & MCP](/docs/getting-started/agents-mcp).
---
# Installation
> Set up Ndop UI in an Angular CLI, Nx or Analog app, then add your first component.
Ndop works in standalone Angular 21 and 22 apps with an `app.config.ts`.
In Angular CLI workspaces, `ndop init` sets everything up, including
Tailwind CSS v4 if your app doesn't have it yet.
## Angular CLI
Create an app, or use an existing one:
```bash
npx @angular/cli new my-app --style tailwind
```
Move into its folder:
```bash
cd my-app
```
Set it up for Ndop:
```bash
npx @ndop/cli init
```
Then add a component:
```bash
npx @ndop/cli add dialog
```
`ndop init` asks before it changes anything. Pass `--yes` to skip the
question, or `--dry-run` to see every change as a diff first.
`ndop init` also adds `@ndop/cli` to your dev dependencies, so later
commands can run your project's own copy, at the version your lockfile
pins: `npx ndop add dialog`, or `pnpm exec ndop add dialog`. Pass
`--no-cli` to leave it out; see [CLI](/docs/getting-started/cli).
## Nx and Analog
`ndop init` reads `angular.json`, so it sets up Angular CLI workspaces only
for now. Nx and Analog apps can use Ndop with a few manual steps, after
which `ndop add` works as usual. Run every command from the workspace root.
### 1. Write `ndop.json`
At the root, pointing at your app's folders:
`ndop.json`:
```json
{
"$schema": "https://ndop.dev/schema/config.json",
"style": "default",
"tailwind": { "css": "apps/my-app/src/styles.css" },
"aliases": {
"ui": "apps/my-app/src/app/ui",
"lib": "apps/my-app/src/app/lib",
"blocks": "apps/my-app/src/app/blocks"
},
"importAlias": "@/",
"iconLibrary": "lucide"
}
```
### 2. Import Tailwind CSS and Ndop's styles
The stylesheet needs Tailwind CSS v4 and Ndop's token mechanics:
`apps/my-app/src/styles.css`:
```css
@import 'tailwindcss';
@import '@ndop/core/styles.css';
```
### 3. Add the theme and the helper files
```bash
npx @ndop/cli add default utils icons
```
### 4. Register Ndop and the icons
In the app's `app.config.ts`:
`apps/my-app/src/app/app.config.ts`:
```ts
import { ApplicationConfig } from '@angular/core';
import { provideNdop } from '@ndop/core';
import { provideIcons } from '@/lib/icons';
export const appConfig: ApplicationConfig = {
providers: [provideNdop(), provideIcons()],
};
```
Without an `@/*` path alias in the root `tsconfig.json`, `ndop add` rewrites
each component's `@/` imports to relative paths.
## What `init` changes
`ndop init` makes these changes, and running it again changes nothing:
- writes `ndop.json`, which records where components go;
- adds the `@/*` path alias to `tsconfig.json`, keeping its comments;
- sets up Tailwind CSS v4, if needed;
- imports `@ndop/core/styles.css` and the default theme's tokens into your
global stylesheet;
- adds `src/app/lib/utils.ts` and `src/app/lib/icons.ts`;
- registers `provideNdop()` and `provideIcons()` in `app.config.ts`;
- installs `@ndop/core` and the theme's fonts with your package manager,
and `@ndop/cli` as a dev dependency unless you pass `--no-cli`.
## Your first component
After `ndop add dialog`, import the parts you need from the file it wrote:
`src/app/app.ts`:
```ts
import { Component } from '@angular/core';
import { DialogContent, DialogTitle, Dialog, DialogPortal, DialogTrigger } from '@/ui/dialog/dialog';
```
Every component page shows complete, tested examples.
---
# Theming
> How Ndop's design tokens work, and how to change colors, radius and fonts.
Ndop styles every component with CSS custom properties that follow
shadcn/ui's names, such as `--background`, `--primary` and `--radius`.
`ndop init` writes the default theme's values into your global stylesheet,
so the theme is yours to edit.
## Where the tokens live
Your stylesheet ends up with four blocks:
`src/styles.css`:
```css
@import 'tailwindcss';
@import '@ndop/core/styles.css';
@theme {
--font-sans: 'Geist Variable', sans-serif;
}
:root {
--background: oklch(1 0 0);
--primary: oklch(0.205 0 0);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.145 0 0);
--primary: oklch(0.922 0 0);
}
```
- `@ndop/core/styles.css` maps the tokens to Tailwind colors, so classes
like `bg-primary` and `text-muted-foreground` work everywhere.
- `:root` holds the light values, `.dark` the dark ones.
- `@theme` sets Tailwind theme values such as fonts.
## Changing the theme
Edit the values in place. Colors use OKLCH, which keeps lightness steady
across hues:
`src/styles.css`:
```css
:root {
--primary: oklch(0.55 0.2 260); /* [!code ++] */
--radius: 0.375rem; /* [!code ++] */
}
```
Every component picks up the change, because none of them hard-code a
color.
## Tokens
| Token | Used for |
| ------------------------------------ | -------------------------------- |
| `--background`, `--foreground` | The page and its text. |
| `--primary`, `--primary-foreground` | Main actions. |
| `--secondary`, `--muted`, `--accent` | Quieter surfaces and hovers. |
| `--destructive` | Actions that delete. |
| `--border`, `--input`, `--ring` | Borders, fields and focus rings. |
| `--radius` | The base corner radius. |
| `--popover`, `--card` | Floating and raised surfaces. |
---
# Dark mode
> Switch between light, dark and the operating system's scheme, without a flash on load.
Ndop supports three color schemes: light, dark, and the operating system's
preference. `provideNdop()` applies one when the app starts, on the server
as well as in the browser, so server-rendered pages arrive in the right
colors.
## Choosing the starting scheme
`src/app/app.config.ts`:
```ts
import { ApplicationConfig } from '@angular/core';
import { provideNdop } from '@ndop/core';
export const appConfig: ApplicationConfig = {
providers: [provideNdop({ theme: 'system' })],
};
```
`system` is the default. It sets `data-ndop-theme="system"` on the root
element, and the `dark:` variant then follows `prefers-color-scheme` in CSS
alone, without any script.
## Switching at runtime
Inject `NdopTheme` and call `set`:
`src/app/theme-toggle.ts`:
```ts
import { Component, inject } from '@angular/core';
import { NdopTheme } from '@ndop/core';
import { Button } from '@/ui/button/button';
@Component({
selector: 'app-theme-toggle',
imports: [Button],
template: ``,
})
export class ThemeToggle {
private readonly theme = inject(NdopTheme);
protected toggle(): void {
this.theme.set(this.theme.mode() === 'dark' ? 'light' : 'dark');
}
}
```
`dark` adds the `dark` class to the root element, which turns on the
`.dark` token block and every `dark:` utility.
## Remembering the choice
Ndop doesn't store the choice, because where to keep it is up to your app.
If you save it, apply it again when the app starts, and before the first
paint if your pages are prerendered, as this site does.
---
# CLI
> The ndop command, its options, and how it keeps every change previewable and repeatable.
The `ndop` CLI sets up your app and adds components to it. Every command
plans its changes first, then applies them: `--dry-run` shows the plan as
diffs, and running a command twice changes nothing the second time.
Run it with your package manager's runner, as in `npx @ndop/cli init`; the
tabs on each command show the pnpm, Yarn and Bun forms. `ndop init` adds
`@ndop/cli` to your dev dependencies, as `ng new` adds `@angular/cli`, so
your project gets its own `ndop` command at the version your lockfile
pins. From then on, run it as `npx ndop add dialog`, which finds the
installed command with npm, pnpm and Bun alike, or as
`pnpm exec ndop add dialog`. The tables below write it as `ndop`. Pass
`--no-cli` to `init` to keep it out of your dependencies and run every
command through `npx @ndop/cli`.
## Commands
| Command | What it does |
| ------------------------ | -------------------------------------------------------------------------------------- |
| `ndop init` | Sets up an Angular app for Ndop. |
| `ndop add ` | Adds components and everything they depend on. |
| `ndop list [words...]` | Lists the registry's components, blocks, themes and libraries. |
| `ndop view ` | Shows an item's API, keyboard map and examples, and how to add it. |
| `ndop diff [items...]` | Shows your changes to installed components, and what changed upstream since. |
| `ndop update [items...]` | Updates installed components, merging upstream's changes into your edits. |
| `ndop doctor` | Checks your setup, and fixes what is safe to fix. |
| `ndop verify [paths...]` | Checks components and pages that use Ndop; see [Verify](/docs/getting-started/verify). |
| `ndop build` | For registry authors: validates items and writes the registry. |
## Options
`init`, `add` and `update` accept:
| Option | Effect |
| ------------------ | --------------------------------------------------------- |
| `--cwd ` | The project root, by default the current folder. |
| `--dry-run` | Prints the changes as diffs; writes and installs nothing. |
| `--json` | Prints one machine-readable result and nothing else. |
| `-y, --yes` | Applies without asking. |
| `--registry ` | Uses another registry; the URL contains `{name}`. |
`init` also takes `--theme `, `--project ` and `--no-cli`; `add` takes
`--overwrite`, and installs a specific version when you write one after
the name, as in `ndop add button@0.1.0`; `update` takes `--force`,
`--continue` and `--interactive`. `doctor` takes the same options, with
`--dry-run` and `--yes` applying to `--fix`, plus `--offline` and
`--project`. For coding agents, `init` takes `--agent`, `--mcp` and
`--verify`; see [Agents & MCP](/docs/getting-started/agents-mcp).
`list`, `view` and `diff` write nothing. They take `--cwd`, `--registry`
and `--json`; `list` also takes `--type`, `view` `--code`, and `diff`
`--to`.
## Browsing the registry
`ndop list` shows what you can add, grouped by type, with each item's
latest version and description. Words after it keep the items that
contain them, and `--type` keeps one type: `ui`, `block`, `theme`, `lib`
or `example`. Examples are listed only with `--type example`, since every
component has several:
```bash
npx @ndop/cli list --type block
```
```text
Blocks (12)
dashboard-01 1.0.0 A dashboard of stat cards, an area char…
dashboard-02 1.0.0 An analytics dashboard with tabs for ov…
data-table-01 1.0.0 A table of tasks to search, filter, sor…
hero-01 1.0.0 A hero section with a headline, two act…
login-01 1.0.0 A sign-in form in a card, with email an…
…
```
It works outside an app, on Ndop's registry. In an app, it also lists the
items of the registries in your `ndop.json`, as `@acme/table`, and shows
which items you installed and at which version. Descriptions are cut to
fit the terminal, 64 columns wide above, and printed in full when the
output goes to a file or another program.
`ndop view` prints what you need to use an item without leaving the
terminal: the command that adds it, its docs page, its dependencies and
files, and for a component its usage, every part's selector and inputs,
its keyboard map, focus rules and examples. The start of its output, with
`…` where lines were left out:
```bash
npx @ndop/cli view button
```
```text
Button button 1.0.0, component
Displays a button or a component that looks like a button.
…
Install npx @ndop/cli add button
Docs https://ndop.dev/docs/components/button
…
API
Button button[ndButton], a[ndButton]
…
Role native (button / link)
[variant] 'default' | 'destructive' | 'outline' |
'secondary' | 'ghost' | 'link' = 'default'
Visual style.
…
Keyboard
Enter Activates the button, or follows the link; does nothing
while disabled.
Space Activates a `button`; does nothing while disabled.
Links don't react to Space.
```
`--code` adds the examples' source. An unknown name fails with the
closest ones, and `--json` prints the item with its files and metadata.
## Themes and URLs
`init --theme` and `add` take a name, a namespaced name such as
`@acme/table`, or the URL of an item's JSON. URLs must use HTTPS, except
on `localhost`. `theme:` in front of any of them checks that the item is a
theme:
```bash
npx @ndop/cli add theme:https://example.com/r/ocean.json
```
Adding a theme to a set-up app replaces the theme variables in your
stylesheet, light and dark. shadcn themes, `registry:style` items with
`cssVars` such as tweakcn's, install the same way.
`ndop.lock.json` records an item added by URL under that URL. `ndop update`
fetches the URL again, and merges your edits using
`@.json` as the base, which `ndop build`
writes. If the site has no such file, files you didn't edit are replaced,
and edited ones are kept and reported, for you to update by hand.
## Your edits are safe
`ndop add` records a hash of every file it writes in `ndop.lock.json`. If
you edit a file and run `add` again, the file is kept and reported as
skipped, unless you pass `--overwrite`. To bring a new version's changes
into a file you edited, use `ndop update`.
## What changed
`ndop diff` shows, for each installed component, two diffs per file: your
changes since you installed it, and what changed upstream since that
version. It writes nothing.
```bash
npx @ndop/cli diff button
```
## Updating
`ndop update` brings components to their latest version without losing
your edits. A file you never changed is replaced. A file you changed is
merged with upstream's changes, the way `git merge` does, using the
version you installed as the common base:
```bash
npx @ndop/cli update
```
When you and upstream changed the same lines, the file gets conflict
markers, with your lines first, and the command exits with code 2. Edit
the file to keep what you want, then record it:
```bash
npx @ndop/cli update --continue
```
The files an update writes must be committed first, so `git checkout`
undoes it; `--force` skips that check. Theme variables you changed keep
your values, new files and dependencies are added, and files a new version
drops stay in your project as yours.
When a new version moves a file, your edited copy moves with it. When it
renames something you use, such as an input, `update` renames it in your
own components and templates as well. To pick which of upstream's changes
to take, file by file, add `--interactive`.
Every component's page links its changelog: what changed in each version,
what `ndop update` does for it, and the diffs. Its update preview merges
your copy of a file in the browser, so you can see the result before
running anything.
## Checking your setup
`ndop doctor` checks everything Ndop relies on and prints each problem with
its fix: the Node and Angular versions, the packages installed components
need, `ndop.json`, the `@/` alias, Tailwind, the theme tokens components
use, `provideNdop()` and the icon provider, the lockfile, zoneless and SSR
support, and the agent files. It writes nothing:
```bash
npx @ndop/cli doctor
```
`--fix` applies the safe fixes, the same edits `init` and `add` make:
installing missing packages, adding the `tsconfig.json` path, adding
missing tokens from your theme, registering the providers and refreshing
the agent files. Add `--dry-run` to see them as diffs first. It never
changes your components or resolves conflicts for you, and running it
again changes nothing.
When `@ndop/cli` isn't in your dependencies, because `init` ran with
`--no-cli` or before it added the package, `doctor` warns, and `--fix`
adds it. A warning doesn't fail the command, so you can leave it out.
It exits with code 1 while any error remains, so CI can run it. `--json`
prints the checks, and `--offline` skips the ones that need the registry,
such as whether your components are behind it.
## Building a registry
`ndop build` reads `registry.json` in `--cwd` and writes the static
registry under `r/` in `--output` (`dist/registry` by default). It fails,
writing nothing, when an item is invalid or doesn't compile. Items that
list `@ndop/` dependencies can import their files; `ndop build`
fetches them from `--registry `, else `NDOP_REGISTRY`, else ndop.dev,
and type-checks against them. See
[Build your own registry](/docs/patterns/build-your-own-registry).
With `--compat shadcn`, it also writes `r/compat/.json` for every
component, block, library and theme, which the shadcn CLI installs in an
Angular app without a `components.json`:
```bash
npx shadcn add https://ndop.dev/r/compat/dialog.json
```
Files go where `ndop init` would put them, under `src/app/ui`,
`src/app/lib` and `src/app/blocks`, with imports between them made
relative. A theme becomes `src/ndop-theme.css`. shadcn then prints the
setup `ndop init` would have done: add a theme and the icons, import
`@ndop/core/styles.css` and the theme in `src/styles.css`, and register
`provideNdop()` and `provideIcons()`. Updates, aliases and agent files stay
`ndop` features.
| Option | Effect |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `--compat shadcn` | Also writes the shadcn-compatible items. |
| `--compat-base-url ` | The absolute URL `r/compat/` is served from, which items' dependencies point at. By default `https://ndop.dev/r/compat/`. |
For `--compat shadcn` to build, every dependency in `registryDependencies`
must be an item of the same registry or `@ndop/`, which points at
Ndop's compat item. That is `https://ndop.dev/r/compat/.json`, or
next to the `--registry` URL when it ends in `{name}.json`.
## Package managers
The CLI installs dependencies with the package manager your project uses,
detected from its lockfile or `packageManager` field. npm, pnpm and bun are
tested on every change.
## For agents and CI
Commands ask for confirmation only in an interactive terminal, and
`--json` prints a single result that a script or a coding agent can read:
```bash
npx @ndop/cli add dialog --yes --json
```
`ndop add` also refreshes the Ndop section of your agents' instructions,
and installs `@angular/cdk` for your Angular version when a component
needs it. The instructions tell agents to run `npx ndop`, or
`npx @ndop/cli` when the project doesn't depend on `@ndop/cli`.
---
# ndop.json
> The project configuration ndop init writes, and every field it can contain.
`ndop init` writes `ndop.json` at the root of your project. The CLI reads it
on every command to know where files go and which registries to use.
`ndop.json`:
```json
{
"$schema": "https://ndop.dev/schema/config.json",
"style": "default",
"tailwind": { "css": "src/styles.css" },
"aliases": {
"ui": "src/app/ui",
"lib": "src/app/lib",
"blocks": "src/app/blocks"
},
"importAlias": "@/",
"iconLibrary": "lucide"
}
```
## Fields
| Field | Meaning |
| -------------- | ---------------------------------------------------------------------------------- |
| `style` | The theme the project was set up with. |
| `tailwind.css` | Your global stylesheet, where tokens and imports go. |
| `aliases` | Folders for components (`ui`), helpers (`lib`) and examples and blocks (`blocks`). |
| `importAlias` | The path alias that points at your app's source, `@/` by default. |
| `iconLibrary` | The icon set `icons.ts` maps names to. |
| `registry` | Another default registry, as a URL containing `{name}`. |
| `registries` | Namespaced registries, for `ndop add @acme/table`, with optional headers. |
| `agents` | The coding agents whose instructions the CLI writes, or `false` for none. |
| `mcp` | Whether the Ndop MCP server is registered in those agents' settings. |
## Private registries
A namespaced registry can send headers, for example a token from an
environment variable:
`ndop.json`:
```json
{
"registries": {
"@acme": {
"url": "https://ui.acme.dev/r/{name}.json",
"headers": { "Authorization": "Bearer ${ACME_TOKEN}" }
}
}
}
```
The CLI expands `${ACME_TOKEN}` from the environment. Installing from
`@acme` when the variable isn't set fails with a message naming it; other
registries keep working. To publish a registry of your own, see
[Build your own registry](/docs/patterns/build-your-own-registry).
---
# Agents & MCP
> How Ndop helps coding agents use its components correctly, with project guidance, an MCP server and a verify loop.
Coding agents write better UI when they don't have to guess an API. Ndop
gives them three things: guidance in your project that's always true for
it, an MCP server with the components' real documentation, and
`ndop verify`, which checks their work and says exactly what to fix.
## Guidance in your project
`ndop init` writes an Ndop section into `AGENTS.md`, which most agents read.
It lists the installed components, the theme's tokens, where files go, and
the workflow: use an installed component first, read its documentation,
give every field a visible label, style with tokens and logical utilities,
then verify. `ndop add` refreshes it every time you install something.
Pick the agents you use with `--agent`, or let `init` detect them:
```bash
npx @ndop/cli init --agent claude-code,cursor
```
| Agent | What `init` writes |
| ------------- | ---------------------------------------------------------------------- |
| Every agent | A section in `AGENTS.md` |
| `claude-code` | The same section in `CLAUDE.md`, and a skill in `.claude/skills/ndop/` |
| `cursor` | A rule in `.cursor/rules/ndop.mdc` |
| `copilot` | The same section in `.github/copilot-instructions.md` |
The CLI only rewrites the text between `` and
``, so your own instructions in those files are safe. The
skill and the rule belong to Ndop; once you edit one, Ndop leaves it alone.
Pass `--agent none` to write nothing.
## The MCP server
The Ndop MCP server gives an agent the registry's components, examples,
blocks and theme tokens, read the way `ndop add` reads them, and can
install items and run `ndop verify` for it. It runs on your machine,
started by the agent.
Let `init` register it for your agents:
```bash
npx @ndop/cli init --agent claude-code --mcp
```
Or add it by hand, as a stdio server that runs `npx -y @ndop/mcp`. For
Claude Code, in `.mcp.json`:
`.mcp.json`:
```json
{
"mcpServers": {
"ndop": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@ndop/mcp"]
}
}
}
```
Cursor reads the same shape from `.cursor/mcp.json`. VS Code reads it from
`.vscode/mcp.json`, under `servers` instead of `mcpServers`.
### Tools
| Tool | What it does |
| --------------------- | -------------------------------------------------------------------------------------------- |
| `get_project_context` | Describes your app: aliases, installed items and versions, files you edited, token names. |
| `search_components` | Finds components by name, purpose or other words for them, such as "modal". |
| `get_component` | A component's documentation: usage, examples, parts, inputs, required nesting, keyboard map. |
| `get_example` | An example's full source. |
| `list_blocks` | Page-level compositions, by category. |
| `get_block` | A block's files and everything it depends on. |
| `add_items` | Installs components or blocks, as `ndop add` does; with `dryRun`, returns the diffs. |
| `validate_usage` | Checks a file's templates against the components' metadata, in a second. |
| `verify` | Runs `ndop verify` and returns every problem with its file, line, rule and fix. |
It also serves each component's documentation as the
`ndop://components/{name}` resource, the theme's tokens as `ndop://tokens`,
and a `build-screen` prompt that walks an agent through building a whole
screen.
### Over HTTP
For agents that connect to a URL, serve it over Streamable HTTP. It listens
on loopback only, and refuses requests from other origins:
```bash
npx -y @ndop/mcp --http
```
The server is then at `http://127.0.0.1:3333/mcp`. `--port` and `--host`
change it, and `--registry` points it at another registry.
## The verify loop
After writing UI, an agent runs [`ndop verify`](/docs/getting-started/verify)
on its files, fixes every problem it reports, and runs it again until it
passes. Each problem names a file, a line and a rule, often with the fix:
```bash
npx @ndop/cli verify src/app/settings --json
```
## Documentation agents can read
Every page of these docs is also Markdown at its URL plus `.md`, such as
[/docs/components/dialog.md](/docs/components/dialog.md).
[/llms.txt](/llms.txt) lists every page, and
[/llms-full.txt](/llms-full.txt) has them all in one file. Each page's menu
copies it as Markdown or opens it in Claude or ChatGPT.
---
# Verify
> ndop verify checks components and pages that use Ndop, for people and for agents, and says exactly what to fix.
`ndop verify` checks the components and pages you or an agent wrote with
Ndop. It reports every problem with its file, line and rule, often with the
fix, and exits with an error until there are none.
```bash
npx @ndop/cli verify src/app/settings
```
Paths can be files or folders; `src/app` by default.
## Install
The checks need a linter and a browser driver, so they live in a separate
dev dependency, `@ndop/verify`, which `ndop init --verify` adds. The render
step also needs Chromium:
```bash
npm install --save-dev @ndop/verify
```
```bash
npx playwright install chromium
```
Without Chromium, every other step still runs, and the result says why
rendering was skipped.
## What it checks
### 1. Usage
Your templates, against the components' metadata: a selector no component
has, with the closest real one; a component that isn't installed, with its
`ndop add` command; a misspelled input; a value outside an input's allowed
ones; a required input left unbound; a part outside the part it must sit
in, such as a dialog title outside its dialog.
### 2. Build
The checked components, built through your app's own `build` target, so
your Angular version, styles, path aliases and strict template checks
apply. Type and template errors come back with their file and line.
### 3. Lint
Ndop's ESLint rules, with your project's own ESLint config when it has one.
See [the rules](#lint-rules).
### 4. Render
Each component in Chromium, in light and dark: axe checks its
accessibility, console errors are reported, and a screenshot of each is
saved to `.ndop/verify/`, which `init` adds to `.gitignore`. Components
with required inputs are skipped, since verify can't invent their data;
verify the page that uses them.
Only `.ndop/verify/` is written to; your files and `angular.json` are never
changed.
## Options
| Option | Effect |
| ------------------ | ------------------------------------------------------------------------- |
| `--skip ` | Skip `usage`, `build`, `lint` or `render`; repeatable or comma-separated. |
| `--json` | Prints one JSON result. |
| `--project ` | The Angular project, when `angular.json` has several. |
| `--cwd ` | The project folder, by default the current one. |
`--skip render` is the quick loop while iterating; the full run takes a
few seconds per component.
## JSON output
With `--json`, verify prints one object an agent can act on:
```json
{
"ok": false,
"steps": [
{ "name": "usage", "ok": false, "durationMs": 5 },
{ "name": "build", "ok": true, "durationMs": 2292 },
{ "name": "lint", "ok": true, "durationMs": 21 },
{ "name": "render", "ok": true, "durationMs": 2000 }
],
"problems": [
{
"file": "src/app/settings/settings.ts",
"line": 12,
"column": 9,
"rule": "usage/wrong-nesting",
"severity": "error",
"message": "DialogTitle must be inside [ndDialog]."
}
],
"screenshots": [".ndop/verify/Settings.light.png", ".ndop/verify/Settings.dark.png"]
}
```
Rules are namespaced by step: `usage/…`, `build/TS2322` or `build/NG8002`,
`ndop/…` for the lint rules, and `axe/…` for accessibility.
## Lint rules
`@ndop/eslint-plugin` holds the rules verify runs. You can also add them to
your own ESLint config, alongside angular-eslint's:
`eslint.config.mjs`:
```js
import angular from 'angular-eslint';
import ndop from '@ndop/eslint-plugin';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
files: ['**/*.ts'],
extends: [...tseslint.configs.recommended, ...angular.configs.tsRecommended],
processor: angular.processInlineTemplates,
},
{
files: ['**/*.html'],
extends: [...angular.configs.templateRecommended],
},
...ndop.configs.recommended,
);
```
| Rule | Checks |
| ----------------------- | --------------------------------------------------------------------------------------- |
| `icon-button-name` | A button whose only content is an icon has `aria-label` or visually hidden text. |
| `dialog-title` | Every dialog has a title. |
| `form-control-label` | Every form control has a label. |
| `logical-properties` | Direction utilities are logical (`ms-2`, not `ml-2`), so layouts mirror; it fixes them. |
| `no-raw-colors` | Colors come from the theme's tokens, not palette colors like `bg-red-500`. |
| `no-private-primitives` | `@ndop/primitives` is imported only through its entry points. |
---
# Migrating from Angular Material
> Move an Angular Material app to Ndop one screen at a time, with a map of components, the theming equivalents and the CDK both build on.
Angular Material and Ndop can run in the same app, so you can move one
screen at a time. Both build their overlays, focus handling and keyboard
support on the Angular CDK, so the two behave alike while they share a page.
This guide was checked against `@angular/material` 22.2.1 and
`@angular/cdk` 22.2.1, read from their published packages.
## What changes
Material's components are installed from npm and styled by its Sass theme.
Ndop's styled items are source files in your app: directives that put
Tailwind classes on elements you write.
```angular-html
```
- **Directives, not elements.** `` renders its own control
and label. `ndCheckbox` goes on your `button`, and its label is a
separate `label` element.
- **Templates, not services.** A Material dialog is a component passed to
`MatDialog.open()`. An Ndop dialog's content is an `ng-template` in the
page, opened by its trigger or by binding `[(open)]`.
- **Models for state.** Open, value and checked state are two-way bindable
models, such as `[(value)]` on Tabs, which takes a string, not an index.
- **Styles are yours.** There are no `*-overrides` mixins: edit the
classes in your copy of the item, or pass `class`, which is merged last.
## Components
| Angular Material | Ndop |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `matButton`, `matIconButton`, `matFab`, `matMiniFab` | [Button](/docs/components/button): `ndButton`. `variant` is `default`, `destructive`, `outline`, `secondary`, `ghost` or `link`, not an appearance. `size="icon"` replaces icon buttons. No FAB. |
| `mat-button-toggle-group`, `mat-button-toggle` | [Toggle Group](/docs/components/toggle-group): `ndToggleGroup`, `ndToggleGroupItem`. `type="single"` or `"multiple"` replaces `multiple`; `value` is always an array. A lone toggle is [Toggle](/docs/components/toggle): `ndToggle` with `[(pressed)]`. |
| `mat-form-field`, `mat-label`, `mat-hint`, `mat-error` | [Form Field](/docs/components/form-field): `ndField`, `ndFieldLabel`, `ndFieldDescription`, `ndFieldError`. No floating label or appearances. `invalid` on the field and its control replaces the error state matcher. |
| `matInput` on `input`, `textarea` | [Input](/docs/components/input): `ndInput`; [Textarea](/docs/components/textarea): `ndTextarea`. The value stays with your forms library: Signal Forms, reactive or template-driven. |
| `mat-checkbox` | [Checkbox](/docs/components/checkbox): `ndCheckbox`, `ndCheckboxIndicator`. On a `button`, with a separate `label`. `[(checked)]` and `[(indeterminate)]` are models. |
| `mat-radio-group`, `mat-radio-button` | [Radio Group](/docs/components/radio-group): `ndRadioGroup`, `ndRadioGroupItem`. `[(value)]` on the group; each item is a `button` with a `value`. |
| `mat-slide-toggle` | [Switch](/docs/components/switch): `ndSwitch`, `ndSwitchThumb`. On a `button`, with `[(checked)]`. |
| `mat-slider`, `matSliderThumb` | [Slider](/docs/components/slider): `ndSlider`, `ndSliderTrack`, `ndSliderRange`, `ndSliderThumb`. Each thumb holds its own `[(value)]`; a range has two thumbs. |
| `mat-select`, `mat-option`, `mat-optgroup` | [Select](/docs/components/select): `ndSelect`, `ndSelectTrigger`, `ndSelectContent`, `ndSelectItem`. Single choice only; for several, use Combobox with `multiple`. |
| `matAutocomplete`, `mat-autocomplete` | [Combobox](/docs/components/combobox): `ndCombobox`, `ndComboboxInput`, `ndComboboxItem`. Filtering is built in, through the `filter` input. |
| `mat-chip-grid`, `matChipInputFor` | [Combobox](/docs/components/combobox) with `multiple`: `ndComboboxChips`, `ndComboboxChip`. Chips show the chosen values; `[(values)]` holds them. For chips that only display, use [Badge](/docs/components/badge). |
| `mat-datepicker`, `matDatepicker`, `mat-date-range-picker` | [Date Picker](/docs/components/date-picker): Popover, `ndDatePickerTrigger` and Calendar. Values are `Date`s, with no `DateAdapter`. `min`, `max` and `disabledDates` on the calendar replace the filter. No typed input. |
| `mat-calendar` | [Calendar](/docs/components/calendar): `ndCalendar`. `mode` is `single`, `multiple` or `range`, with `[(value)]`, `[(values)]` or `[(range)]`. |
| `MatDialog`, `mat-dialog-title`, `mat-dialog-content`, `mat-dialog-actions`, `mat-dialog-close` | [Dialog](/docs/components/dialog): `ndDialog`, `ndDialogContent`, `ndDialogTitle`, `ndDialogFooter`. Declared in the template, not opened by a service. `[closeOnEscape]` and `[closeOnOverlayClick]` replace `disableClose`. Confirmations use [Alert Dialog](/docs/components/alert-dialog). |
| `MatBottomSheet` | [Drawer](/docs/components/drawer) or [Sheet](/docs/components/sheet) with `side="bottom"`. Declared in the template, like Dialog. |
| `mat-sidenav-container`, `mat-sidenav`, `mat-drawer` | [Sidebar](/docs/components/sidebar): `ndSidebarProvider`, `ndSidebar`, `ndSidebarInset`. `collapsible` is `offcanvas`, `icon` or `none`; on phones the sidebar opens as a modal sheet. For a panel over the page, use Sheet. |
| `mat-menu`, `matMenuTriggerFor`, `mat-menu-item` | [Dropdown Menu](/docs/components/dropdown-menu): `[ndDropdownMenuTrigger]="menu"`, `ndDropdownMenuItem`. The trigger takes an `ng-template`, as `matMenuTriggerFor` takes a menu; items emit `(triggered)`. |
| `matContextMenuTriggerFor` | [Context Menu](/docs/components/context-menu): `[ndContextMenuTrigger]="menu"`. Same shape as Dropdown Menu. |
| `matTooltip` | [Tooltip](/docs/components/tooltip): `ndTooltip`, `ndTooltipTrigger`, `ndTooltipContent`. The content is a template, not a string. `side` and `align` replace `matTooltipPosition`; `openDelay` and `closeDelay` replace the show and hide delays. |
| `mat-tab-group`, `mat-tab` | [Tabs](/docs/components/tabs): `ndTabs`, `ndTabsList`, `ndTabsTrigger`, `ndTabsContent`. Tabs are named by `value`, not by index. Tab links (`mat-tab-nav-bar`) have no equivalent. |
| `mat-accordion`, `mat-expansion-panel` | [Accordion](/docs/components/accordion): `ndAccordion`, `ndAccordionItem`, `ndAccordionTrigger`. `multiple` replaces `multi`. A single panel is [Collapsible](/docs/components/collapsible). |
| `mat-card` and its parts | [Card](/docs/components/card): `ndCard`, `ndCardHeader`, `ndCardTitle`, `ndCardContent`, `ndCardFooter`. `ndCardDescription` replaces the subtitle; `ndCardFooter` holds the actions. |
| `mat-divider` | [Separator](/docs/components/separator): `ndSeparator`. `orientation="vertical"` replaces `vertical`. |
| `mat-progress-bar` | [Progress](/docs/components/progress): `ndProgress`, `ndProgressIndicator`. Indeterminate while `value` is `null`. No buffer mode. |
| `mat-progress-spinner`, `mat-spinner` | [Spinner](/docs/components/spinner): `ndSpinner`. Indeterminate only, with a `label` for screen readers. |
| `MatSnackBar` | [Toast](/docs/components/toast): `Toaster`, `ndToaster`. `inject(Toaster).show({ title, action })`; the toast region goes once in your app's shell. |
| `matBadge` | [Badge](/docs/components/badge): `ndBadge`. A label of its own, not an overlay on another element; on an avatar, use `ndAvatarBadge`. |
| `mat-table`, `matSort`, `mat-sort-header` | [Table](/docs/components/table) and [Data Table](/docs/components/data-table): `createDataTable`. A native `table` with `@for` over `rows()`; sorting, search, pages and selection are signals. |
| `mat-paginator` | [Pagination](/docs/components/pagination): `ndPagination`, `ndPaginationLink`. Links or buttons you render; a data table's `pageIndex` and `pageCount` drive them. |
| `mat-icon` | `ndIcon` from `@ndop/core`. `