Guide

Updating installed components

Ouro HX components are copied into your project, so they're yours to edit. To take an upstream change, compare first, then decide.

  1. 1. Review the change

    List the files the registry's current version would change:

    npx shadcn@latest add @ouro/sheet --dry-run

    Then see the difference for each of those files by its path:

    npx shadcn@latest add @ouro/sheet --diff src/components/ouro/sheet/sheet.tsx

    Without the namespace, use ourostack/ouro-ui/sheet#v1.2.13 in place of @ouro/sheet. A bare --diff without a path can stop after the first few unchanged files, before the one you edited.

  2. 2. Overwrite, then reapply your edits

    The CLI replaces files; it doesn't merge. Commit first, so your edits are safe, then overwrite:

    npx shadcn@latest add @ouro/sheet --overwrite
    git diff

    The diff shows the upstream change and, mixed in, your own edits being removed. Bring back the ones you still want.

  3. 3. Run the tests that came with it

    A component's unit tests, stories and end-to-end specs are its optional -tests item. If you installed them, update them with the component, then run them before you commit:

    npx shadcn@latest add @ouro/sheet @ouro/sheet-tests --overwrite

    To add them, run the same command without --overwrite. ouro-tests adds every component's tests.

  4. Upgrading from v0.1.0

    v0.1.0 installed each component with its tests, and v0.2.0 moves some shared helpers to new files, so update every Ouro component in one go.upgrade-v0.1.0upgrade-v0.2.0 Run the commands in your app's root, the folder with components.json. They find your folders the way shadcn does, from the aliases in components.json and the paths in your tsconfig (npx shadcn@latest info shows them), so they work in a src/ or an app/ layout as written.

    What changed, by version

    What an app on v0.1.0 has to know beyond these steps.upgrade-test The release notes list every change.upgrade-releases

    • **v0.2.0:** each component's tests became an optional <name>-tests item, some helpers moved to new files (step 4), and v0.1.0's self-referential theme lines can go (step 2).upgrade-v0.2.0
    • **v0.3.0:** sheets and dialogs stack at --ouro-layer, a z-index of 50 by default, instead of level 0, so an app element at z-index 1 to 49 that used to show above an open sheet or dialog now sits under it.upgrade-v0.3.0 Raise the token in a :root rule of your own if your app has fixed bars above z-50 (see "Theme" in the README), and drop any portal z-index workaround. A controlled Dialog opened by a touch or pen tap focuses its popup instead of its first input, and ConfirmDialog's finalFocus applies only after a confirmed action; drop workarounds for either. Every coverage hint is /* istanbul ignore … @preserve */, so a build plugin that kept the hints can go.upgrade-hints A -tests item sets Vitest to ^5.
    • **v0.3.1:** every component starts with "use client", so Next.js App Router pages can render them, and the @ouro namespace serves the newest release instead of main.upgrade-v0.3.1
    • **After v0.7.0:** a required field's * is drawn with CSS on a new element around the label and is no longer a <span data-ouro-required> inside it, so tests that expected the label text "Email*" now find "Email".upgrade-v0.8.0 The clear, show-password and choose-date buttons in TextField and DatePicker are named by hidden text, not aria-label, so replace button[aria-label="…"] selectors with [data-ouro-clear], [data-ouro-show-password] or [data-ouro-choose-date]. The same goes for a file's remove button in FileField ([data-ouro-file-remove]), Combobox's clear and chip remove buttons ([data-ouro-combobox-clear], [data-ouro-combobox-chip-remove]) and NumberField's steppers ([data-ouro-increment], [data-ouro-decrement]). A remove button's hidden text holds the file or item name, so a loose getByText("pasta.png") now finds it too; pass { exact: true }. FieldLabel's className now goes on the new element around the label, which also carries [data-ouro-required], and a group's label id is on the inner <span>; style, id, data-* and handlers still go on the <label>. A numeric size on TextField is now a type error and a development warning; set a width with className="w-40". FormErrors values are now ErrorMessages | null | undefined, so code that reads errors.email as string | string[] no longer type checks until it handles null and React nodes.upgrade-v0.8.0 For action answers, import formErrors from @/lib/ouro/form-answer and pass errors={formErrors(actionData, actionData?.errors)} from the initial render; this replaces the unreleased separate result prop. It identifies equal new answers while keeping cleared fields masked on pending/client renders and rendering current uncleared messages. Cached/reused answers need { key: acceptedAnswerKey }, changed on actual acceptance, not submit start. Raw records remain compatible but content-only. Reconstruct the value beside Form after JSON/spread/cloning or server-component transport: only messages serialize, not private identity; a safely transported acceptance key can identify the locally reconstructed answer. On a phone, a Link that is not in running text is now a box at least 44px tall and wide that takes its room in the layout, so a column of links grows; pass inline to a link in a sentence inside your own <div> or <span>.upgrade-link-box A Form's top message element exists only while there is a message. Component roots no longer set m-0, so a parent's space-y-* now spaces them; remove any margin you added to work around that.upgrade-v0.8.0
    • **v0.8.0:** no component installs ouro-base; add it once (step 1 does), and later adds leave your stylesheet alone.upgrade-v0.8.0 A component installs only the helpers it imports, one item per file in lib/ouro, and a -tests item brings the tests of everything its component installs.upgrade-v0.8.0 The -tests items no longer set a version for Vitest, Storybook or Playwright.upgrade-test-versions The specs import test and expect from e2e/ouro-fixtures.ts, a file of yours (step 1).upgrade-v0.8.0 lib/ouro/use-dev-warnings.ts is now lib/ouro/use-dev-messages.ts (step 4).upgrade-v0.8.0 Earlier releases installed every helper in lib/ouro with any component, and an update deletes nothing: audit references before deleting unused historical helpers and their own tests (step 5).upgrade-v0.8.0 OuroProvider now comes only with the components that render links and with tooltip; your copy stays, and a new app adds lib-ouro-provider by name.
    • **After v0.8.2:** ouroTestProjects({ page }) also sizes the touch project's Playwright host, preventing Vitest 4 from scaling landscape touch frames.upgrade-touch-host The logical touch viewport still starts at 390x844 with mobile/touch input, but an explicitly enlarged host changes outer-window/screen dimensions and screen orientation; do not treat those as hardware-phone measurements.upgrade-touch-host Without page, the touch host stays 390x844.upgrade-touch-host On Vitest 4, retain a host large enough for every requested test viewport, such as the Testing guide's 1600x1200 example.upgrade-touch-host
    • **v1.0.0:** the first stable release keeps the component APIs, theme tokens, defaults and test-tool minimums from v0.8.4.upgrade-v1.0.0 An app already on v0.8.4 needs no component API migration: review the update with --dry-run and --diff, then overwrite only the items it already uses and reapply its local edits.upgrade-v1.0.0 Runtime components and their optional -tests items remain separate; keep your app-owned fixtures and test configuration. Apps on older releases still need the applicable migration steps above and below.
    • **After v1.0.2:** sheets, and a Menu shown as a bottom sheet, read their shadow from --ouro-shadow-sheet-<side>, and Checkbox's tick draws over --ouro-duration-draw, both of which only a fresh ouro-base adds (step 1 adds it again); without it a sheet draws no shadow and the tick appears at once.upgrade-v1.1.0 Every other shadow reads shadcn's --shadow-* tokens, and headings and overlay titles --font-heading, so a theme that sets them now restyles Ouro too.upgrade-v1.1.0upgrade-theme-tokens
    • **After v1.0.1:** formErrors now declares its already-frozen message arrays readonly.upgrade-v1.0.2 Keep mutable FormErrors and ErrorMessages for raw builders; annotate returned snapshots as FormErrorValue or ReadonlyFormErrors, or let their type infer. Form, EditSheet, field error props and the normalization helper accept readonly projections. This corrects a false mutation permission, so a snapshot no longer fits a mutable raw-map annotation.upgrade-v1.0.2 TypeScript's Array.isArray narrows to any[] and can bypass readonly; unchanged runtime freezing still rejects container mutation, without deeply freezing React nodes.
    • **After v1.2.2:** Button accepts reloadDocument for a document request through an internal href, and its downloads use a plain anchor like Link's.upgrade-document-download A false or null download no longer forces document navigation; pass reloadDocument when a resource route needs it without a download.upgrade-document-download
    • **After v1.2.4:** Select, Menu, Combobox, Popover, Tooltip, HoverCard and NavigationMenu open their popups at the end of <body> unless OuroProvider is given a portalContainer, and in development each warns once when it opens outside every landmark of a page with a <main>.upgrade-portal-container To keep popups in a landmark, make const popups = useRef<HTMLDivElement>(null), wrap the app in <OuroProvider portalContainer={popups}> and render <div ref={popups} /> last inside <main>, outside any element with a transform, filter or contain, which would clip or move the popups. In a Next.js App Router layout, which is a server component, put the provider and the ref in a small client component. Inside a dialog your app draws itself, wrap its content in <OuroProvider portalContainer={null}> so its popups open where Base UI puts them.upgrade-portal-null Dialogs, sheets and toasts still open at the end of <body>. The landmark warning goes to console.warn, so a test that fails on console output fails until the popups are in a landmark. SidebarLayout's header now renders in a <header>, the page's banner, so remove a banner your page draws around SidebarLayout.upgrade-sidebar-header The testing/axe kit has a new audit, which fails every violation at any impact and every result axe could not decide unless it measures it, in both color schemes.upgrade-audit The ouro-testing item now names axe-core among its devDependencies, so an update adds it to an app that lacks it.upgrade-axe-core
    • **After v1.2.8:** the Toaster's Notifications region is no longer a live region: two live regions last inside it say each toast's message and description, the assertive one a role="alert", so for its first seven seconds a toast's words are in the page twice.upgrade-toast-announcer Find a toast in a test by its role and name, getByRole("dialog", { name: "Saved" }), or "alertdialog" for a destructive toast, not with getByText, and expect no aria-live on the region. Dismiss buttons and destructive toasts are no longer hidden from screen readers while the stack is collapsed.upgrade-toast-reachable The testing/axe kit's audit now leaves focus, the selection and every scroll position as it found them, and throws, naming what changed, when its Tab walk closes a popup it cannot open again; audit the page before that popup opens.upgrade-audit-leaves

    0. Commit first

    --overwrite replaces every Ouro file, including files you edited.upgrade-shadcn-add Commit before you start. Afterwards, git diff shows your own edits being overwritten, so reapply the ones you need.

    Review the diff of your Tailwind CSS file too, the one components.json names in tailwind.css, because step 1 adds ouro-base, the one item that writes there, and shadcn add rewrites more than Ouro's tokens. It adds @custom-variant dark (&:is(.dark *)); when the file has no @custom-variant (shadcn@4.21.1 does this for any item with theme variables on Tailwind CSS v4), adds the tokens your file lacks to :root, .dark and @theme inline (among them --input, --radius and the radius scale, if your theme has none), and collapses blank lines.upgrade-shadcn-css Keep the new lines and step 2's deletions, and revert the rest. After this upgrade no component add opens the file again.upgrade-v0.8.0

    1. Update every Ouro component, with its tests

    Update every Ouro component in your app, not only the items you typed: v0.1.0 added each component's dependencies too, so an app that added only dialog also has button and sheet. Each component is one .tsx file in your components/ouro/ folder (dialog/ holds dialog and confirm-dialog, and text/ holds text and heading).upgrade-v0.1.0 Add each one as <name> and <name>-tests, plus ouro-base, in one command at the same ref, with --overwrite (without it the CLI stops to ask about each file). For the app that added only dialog, that is ouro-base button button-tests dialog dialog-tests sheet sheet-tests. No component installs ouro-base any more, so this is where your stylesheet gets the tokens added since v0.1.0.upgrade-v0.8.0

    This command finds the components, prints them and adds them. If it finds none, it stops before adding anythingupgrade-none; run it in the folder with components.json. If you use the Ouro theme, add ourostack/ouro-ui/ouro-theme#v1.2.13 to the add in it.

    ( p() { npx shadcn@latest info --json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).config.resolvedPaths.$1"; }; ouro="$(p components)/ouro"; items=$(find "$ouro" -name '*.tsx' ! -name '*-parts.tsx' ! -name '*.test.tsx' ! -name '*.stories.tsx' 2>/dev/null | sed -E 's|.*/(.*)\.tsx$|\1|' | sort); [ -n "$items" ] || { echo "No Ouro components in $ouro. Run this in the folder with components.json." >&2; exit 1; }; echo "Updating: $(echo "$items" | paste -sd " " -)"; npx shadcn@latest add $(echo "$items" | sed -E 's|.*|ourostack/ouro-ui/&#v1.2.13 ourostack/ouro-ui/&-tests#v1.2.13|') ourostack/ouro-ui/ouro-base#v1.2.13 --overwrite )

    This keeps the tests v0.1.0 installed, deletes none of your files and adds no component you didn't have. shadcn may print Skipped N files: (files might be identical, use --overwrite to overwrite) even with --overwrite: those files already equal the release's, so nothing was missed.upgrade-shadcn-add The -tests items name Vitest, Storybook and Playwright without a version, so the versions in your package.json stay as they are. v0.7.0's items still carry versions: after an add at v0.7.0, restore every test-tool version you had pinned (git diff package.json): Vitest and its companions, Playwright and Storybook.upgrade-v0.7.0 The tests need Vitest 4.1.11 or later, Playwright 1.58.1 or later and Storybook 10.2.10 or later; update those yourself if yours are older (the README's "Details" links the Testing page).upgrade-minimums The specs in e2e/ouro now import test and expect from e2e/ouro-fixtures.ts, a file of yours: add it with the one line export { expect, test } from "@playwright/test";, or export your own fixtures from it.

    2. Remove the self-referential theme lines

    v0.1.0 wrote a line into @theme inline for each Ouro token that points at itself, such as --ouro-press-scale: var(--ouro-press-scale);, and --color-ouro-backdrop-color: var(--ouro-backdrop-color);.upgrade-self-reference Delete them from your Tailwind CSS file, the one components.json names in tailwind.css. The values stay in :root and .dark. This command finds the file and deletes the lines:

    perl -i -ne 'print unless /^\s*--(color-)?ouro-[\w-]+:\s*var\(--ouro-[\w-]+\);\s*$/' "$(npx shadcn@latest info --json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).config.resolvedPaths.tailwindCss")"

    3. Optional: drop Ouro's tests

    To keep only the components, delete the files Ouro owns for its tests: the test, type-test and story files in your components/ouro/ and lib/ouro/ folders, the test kit in lib/ouro/testing and the specs in e2e/ouro. The command stops if it can't find those folders.

    ( p() { npx shadcn@latest info --json | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).config.resolvedPaths.$1"; }; c="$(p components)/ouro"; l="$(p lib)/ouro"; [ -d "$c" ] && [ -d "$l" ] || { echo "No Ouro folders at $c and $l. Run this in the folder with components.json." >&2; exit 1; }; find "$c" "$l" \( -name '*.test.ts' -o -name '*.test.tsx' -o -name '*.test-d.ts' -o -name '*.stories.tsx' \) -delete; find "$l/testing" e2e/ouro -delete )

    Then remove the ouroTestProjects import and call from your Vitest config. Only if Ouro's tests were the only tests using them, also delete vitest.config.ts, playwright.config.ts and .storybook/, and uninstall vitest, @vitest/browser-playwright, playwright, @playwright/test, storybook and @storybook/react-vite with your package manager.

    4. Change imports of moved helpers

    If your own code imports these helpers, change the import:

    • useOuro: from lib/ouro/ouro-provider to lib/ouro/use-ouro.
    • sheetSides, sideWarning, sheetHint, iosBackdrop and footerClass: from components/ouro/sheet/sheet-parts to components/ouro/sheet/sheet-styles.
    • dialogSizes and dialogSizeWarning: from components/ouro/dialog/dialog-parts to components/ouro/dialog/dialog-styles.
    • alignClasses and checkAlign: from components/ouro/text/text to components/ouro/text/text-styles.
    • useDevWarnings: from lib/ouro/use-dev-warnings to lib/ouro/use-dev-messages. Delete the old lib/ouro/use-dev-warnings.ts; nothing imports it after step 1.

    5. Delete the helpers nothing imports

    Releases before v0.8.0 installed every helper in lib/ouro with any component.upgrade-v0.8.0 From v0.8.0 a component installs only the helpers it imports, but an update deletes nothing.upgrade-v0.8.0 Remove unused historical helpers rather than excluding them from coverage.

    Before deleting anything, perform a read-only import/reference audit across the whole app, including scripts, tests, stories and configuration outside src/. Use the app's actual tsconfig and bundler aliases to resolve references to each helper's full path. Check relative imports, explicit extensions, re-exports and dynamic imports too; an importing file with the same basename is still a different file. A text search for lib/ouro/<name> alone cannot prove a helper is unused.

    Delete a helper and its own tests only after verifying that no remaining app file or retained Ouro file references it (references from that helper's own tests do not require keeping an otherwise unused helper). Check again after each removal: another historical helper may have been used only by the one you removed. If a reference is unresolved or you cannot verify a file's use, keep it until you can. Keep ouro-provider.tsx if you mean to use it and have not yet, and retain ambient declarations such as env.d.ts that the app's type check needs even without an import.

    Then type check and build your app. CI runs steps 1 to 3 on a v0.1.0 app, with these commands read from this file (scripts/install-test.sh upgrade).upgrade-test

    Claim sources

  5. Pin a version

    Add a tag to pin a release, so a later release doesn't change what you install:

    npx shadcn@latest add ourostack/ouro-ui/sheet#v1.2.13

    A release tag pins the item and every Ouro dependency it brings (such as button and the helpers in lib/ouro) to the same tag. Any other ref, #<sha> or #<branch>, pins only the item you name: its Ouro dependencies come from main. The @ouro namespace serves the newest release, v1.2.13. Without a ref, the GitHub address installs main, which is for contributors and can hold unreleased changes. Releases are listed on GitHub.