Migrating from spartan/shadcn
Bring Ndop into an app that uses spartan/ui or shadcn-style components, keep your theme, and let ndop take over files you copied.
Ndop follows shadcn/ui closely: its styled items use the classes of
shadcn/ui's new-york-v4 style, its themes use shadcn's token names, and its
registry format is a superset of shadcn's. Coming from spartan/ui or from
shadcn itself, most of what you know carries over. What Ndop adds is
ndop update, which merges new versions into the files you've edited.
This guide was checked against spartan/ui 1.6.3 (@spartan-ng/brain and
@spartan-ng/cli), read from their published packages, and the shadcn CLI
4.21.4, the version Ndop's install test runs.
What carries over
Tokens.
--background,--primary,--radiusand the rest have shadcn's names, in:rootand.dark, so a shadcn or spartan theme's values paste in unchanged.ndop addalso installs shadcn themes published asregistry:styleitems, such as tweakcn's:npx @ndop/cli add theme:https://example.com/r/ocean.jsonpnpm dlx @ndop/cli add theme:https://example.com/r/ocean.jsonyarn dlx @ndop/cli add theme:https://example.com/r/ocean.jsonbunx @ndop/cli add theme:https://example.com/r/ocean.jsonClasses. Each styled item's
variants()table holds the class strings of the shadcn/ui new-york-v4 item it follows, and a test per item compares the two. spartan 1.6 keeps its classes in a stylesheet per design style instead (.style-vegaby default), so expect small visual differences from spartan.cn()and variants.cn()merges classes with clsx and tailwind-merge, as shadcn's does.variants()takes the same parts as cva:base,variants,compoundVariantsanddefaults.Names. Parts keep shadcn's names:
DialogContent,CardHeader,SelectTrigger. In templates they're attributes with thendprefix, such asndDialogContent.The registry. Items have shadcn's fields (
files,dependencies,registryDependencies,cssVars) plus Ndop's own, andndop.jsontakes namespacedregistriesascomponents.jsondoes; see ndop.json.
What changes
- Directives only. spartan renders some parts as elements, such as
<hlm-checkbox>,<hlm-dialog>and<hlm-select-trigger>. Every Ndop part is a directive on an element you write:<button ndCheckbox>,<ng-container ndDialog>,<button ndSelectTrigger>. - Portals are templates. Content that opens over the page, such as a
dialog's or a tooltip's, goes in an
ng-templatewith a portal directive, as Radix'sPortaldoes in shadcn. - No
asChild. Put the directive on the element itself:<a ndButton href="/docs">instead of a slot. - One import path per component.
ndop add dialogwritessrc/app/ui/dialog/dialog.ts, imported as@/ui/dialog/dialog. - Icons. Items ask for icons by name,
<svg ndIcon="x">, from Lucide through youricons.tsfile.
Components
spartan/ui's items and Ndop's mostly share names, and attribute parts map
hlm<Part> to nd<Part>: hlmCardTitle becomes ndCardTitle. The table
lists where they differ.
| spartan/ui | Ndop |
|---|---|
hlmBtn |
Button: ndButton. Same variants and sizes. |
hlm-checkbox, hlm-switch |
Checkbox, Switch. Directives on a button, with an ndCheckboxIndicator or ndSwitchThumb inside. |
hlm-radio-group, hlm-radio |
Radio Group: ndRadioGroup, ndRadioGroupItem. Each item is a button with a value. |
hlm-slider |
Slider: ndSlider, ndSliderTrack, ndSliderRange, ndSliderThumb. You write the track, range and thumbs. |
hlm-select and its parts |
Select: ndSelect, ndSelectTrigger, ndSelectValue, ndSelectItem. Single choice; for several, use Combobox with multiple. |
hlm-combobox, hlm-autocomplete |
Combobox: ndCombobox, ndComboboxInput, ndComboboxItem. One item covers both, with chips for multiple. |
hlm-dialog, hlm-dialog-content, hlmDialogTrigger |
Dialog: ndDialog, ndDialogPortal, ndDialogContent. Content goes in an ng-template ndDialogPortal. ndDialogCloseButton is the corner close button. |
hlm-alert-dialog, hlm-sheet, hlm-drawer |
Alert Dialog, Sheet, Drawer. Same shape as Dialog. |
hlmPopover, hlmHoverCard |
Popover, Hover Card. Content goes in a portal template. |
hlmTooltip |
Tooltip: ndTooltip, ndTooltipTrigger, ndTooltipContent. The content is always a template, in an ndTooltipPortal. |
hlmDropdownMenu, hlmDropdownMenuTrigger, hlmContextMenuTrigger, hlmMenubar |
Dropdown Menu, Context Menu, Menubar. Each trigger takes an ng-template holding the menu's content part, such as ndDropdownMenuContent, and each component has its own item parts. |
hlmTabs, hlmTabsTrigger, hlmTabsContent |
Tabs: ndTabs, ndTabsTrigger, ndTabsContent. [(value)] on ndTabs, and a value input on each trigger and panel, name the tabs. |
hlm-command-input, hlmCommandGroupLabel |
Command: ndCommandInput, ndCommandGroupHeading. The input is an input inside an ndCommandInputWrapper. |
hlmInputOtp, hlm-input-otp-slot |
Input OTP: ndInputOtp, ndInputOtpInput, ndInputOtpSlot. One real input, plus a slot per character with its index. |
hlmAccordion and its parts |
Accordion: ndAccordion, ndAccordionTrigger. multiple replaces type; the trigger sits in an ndAccordionHeader. |
hlmField and its parts |
Form Field: ndField, ndFieldLabel, ndFieldError. The item is called form-field. |
hlm-avatar, hlm-spinner, hlm-breadcrumb-ellipsis, hlm-pagination-next |
Avatar, Spinner, Breadcrumb, Pagination. Attributes instead of elements, with the same names. |
hlm-calendar, hlm-calendar-range, hlm-calendar-multi |
Calendar: ndCalendar. One calendar with mode="single", "range" or "multiple", and its grid parts in your template. |
hlm-date-picker and its variants |
Date Picker: Popover, ndDatePickerTrigger and Calendar. Composed from those three, with no typed input. |
hlm-toaster, toast() |
Toast: ndToaster, Toaster. inject(Toaster).show({ title }) instead of toast(). |
hlmResizableGroup, hlm-resizable-handle |
Resizable: ndResizablePanelGroup, ndResizableHandle. Sizes are a [(sizes)] model on the group. |
ng-scrollbar[hlm] |
Scroll Area: ndScrollArea. Styles the native scrollbar; no ngx-scrollbar. |
tanstack-chart[hlmChart] |
Chart: ndChartContainer, ndChartPlot. Ndop's own SVG parts, with no chart library. |
hlm-sidebar and its parts |
Sidebar: ndSidebarProvider, ndSidebar. ndSidebarProvider holds the open state. |
hlmTable and its parts |
Table: ndTable, ndTableRow, ndTableCell. The long names only; hlmTr and hlmTd have no short form. Sorting and pages come from Data Table. |
Alert, Aspect Ratio, Badge, Card, Collapsible, Empty, Input, Label,
Navigation Menu, Progress, Separator, Skeleton, Textarea, Toggle and Toggle
Group follow the hlm to nd rule; each component's page lists its parts.
spartan's attachment, bubble, button group, carousel, input group, item, kbd, marker, message, message scroller, native select, questionnaire and typography have no Ndop item.
Setting up Ndop in your app
ndop init works in an existing Angular CLI app, alongside the components
you have. spartan's files stay where its components.json put them, and
Ndop's go where ndop.json says, src/app/ui by default.
Restore the Tailwind import
ndop initrecognizes Tailwind by@import 'tailwindcss'in your global stylesheet. spartan'sinitreplaces that line with imports of Tailwind's parts; put it back, so Tailwind isn't added a second time:src/styles.css@layer theme, base, components, utilities; /* [!code --] */ @import 'tailwindcss/theme.css' layer(theme); /* [!code --] */ @import 'tailwindcss/preflight.css' layer(base); /* [!code --] */ @import 'tailwindcss/utilities.css'; /* [!code --] */ @import 'tailwindcss'; /* [!code ++] */Run init
Commit your work, then preview the changes and apply them:
npx @ndop/cli init --dry-runpnpm dlx @ndop/cli init --dry-runyarn dlx @ndop/cli init --dry-runbunx @ndop/cli init --dry-runnpx @ndop/cli initpnpm dlx @ndop/cli inityarn dlx @ndop/cli initbunx @ndop/cli initinitimports@ndop/core/styles.cssafter your other imports. Leave it after spartan's preset: both define Tailwind'sdarkvariant, the last definition wins, and Ndop's is the one that also follows the system scheme.Keep one theme
initwrites the default theme's values into:root, replacing variables with the same names, and adds.darkblocks. spartan's:root.darkblock is more specific than.dark, so in dark mode its values would still win. Delete it, then put the values you want in Ndop's blocks: your old ones, whichgit diffshows, or a theme from the gallery.
From then on, spartan's and Ndop's components work side by side, so you can replace them a screen at a time.
Files you copied
If you copied Ndop's files by hand, from a component page's Manual tab, or
installed them with the shadcn CLI from https://ndop.dev/r/compat/, let
ndop take them over, so ndop update can merge new versions into them.
ndop add writes a file only when it's new, or still matches what Ndop
installed. A copy that differs, even only in its import paths, is reported
as skipped and not recorded:
npx @ndop/cli add dialogpnpm dlx @ndop/cli add dialogyarn dlx @ndop/cli add dialogbunx @ndop/cli add dialogTo adopt it, record the version you copied as its base, then put your copy back:
Install the version you copied
With your work committed, overwrite the copy with that version. Each component's changelog lists its versions.
npx @ndop/cli add dialog@0.1.0 --overwritepnpm dlx @ndop/cli add dialog@0.1.0 --overwriteyarn dlx @ndop/cli add dialog@0.1.0 --overwritebunx @ndop/cli add dialog@0.1.0 --overwriteRestore your copy
git restore src/app/ui/dialog/dialog.tsndop.lock.jsonnow records that version and its hash, and your file counts as an edit of it.--overwritealso replaces edited files of the items it depends on, such asutils.ts;git statuslists them, andgit restorebrings them back the same way.Update
Commit, then update:
npx @ndop/cli update dialogpnpm dlx @ndop/cli update dialogyarn dlx @ndop/cli update dialogbunx @ndop/cli update dialogndop updatemerges the changes made upstream since that version into your copy, keeping your edits, the waygit mergedoes. Where you and upstream changed the same lines, it writes conflict markers for you to resolve.
If you don't know which version you copied, install the latest instead.
ndop diff dialog then shows every difference as your edit, including
upstream changes your copy predates, so review it once.
Next steps
- CLI covers
ndop diffandndop updatein detail. - Theming lists every token.
- Build your own registry if you publish components of your own.