Documentation / Design & UI
Loading and motion
The template has two loading treatments, both registered in /components and built from the same source used in the application:
| Situation | Component | Behaviour |
|---|---|---|
| Content is arriving for the first time | ContentSkeleton | Reserve the incoming layout with the shared skeleton animation. |
| A user has started an action | PendingButton | Keep the button's size stable and move through pending, success, and error in place. |
| That action belongs to a switch, selection, or menu | PendingIndicator | Show the same pending and completion treatment beside the affected control. |
Do not replace already loaded content with skeletons during a save or update. Keep the current form, list, or selection visible; put the busy state on the affected control. Never show another user's or team's data while changing context. Auth and authorization remain with the existing providers and connected components.
The shared PublicShell keeps public resource navigation outside page loading and error boundaries, with no account controls or session lookups. The connected AppShell keeps its optional team, account menu, and entry actions inside independent AccountBoundary components. AccountBoundary.errorFallback can supply a layout-appropriate recovery surface: the application header uses a compact account-error popover containing the existing AccountLoadError reload action. Private content remains behind the /app guard and its identity-keyed data provider.
Incoming content
Import ContentSkeleton from @/components/loading/content-skeleton. Its variants are page, section, form, list, table, auth, avatar, and team-switcher. Pick the closest incoming layout and give it a specific accessible label:
<Suspense fallback={<ContentSkeleton variant="form" label="Loading profile" />}>
<ProfileSettings />
</Suspense>
For a new shape, compose the registered Skeleton primitive. Keep the shapes and dimensions close to the real content, and put a single accessible status on their containing region. Decorative skeleton shapes do not need individual announcements. Add a new reusable layout to ContentSkeleton and its gallery example instead of introducing another loading component.
Skeletons are only for unresolved content. Empty results show an empty state. Failed requests show a truthful error and a retry action. Render ready data immediately; do not make the user wait to complete an animation.
Actions
PendingButton wraps our Base UI Button and accepts its regular styling, size, and interaction props. Its status is idle, pending, success, or error; labels describe the actual operation. It reserves room for the different labels and icons so its footprint stays stable. Icon-only actions keep their compact size and require an accessible name.
Its feedback icons inherit the button's foreground, including errors, so filled primary and outline variants remain readable in both themes. Keep error details and retry guidance beside the action using destructive text. A standalone PendingIndicator retains its destructive error color. The /components/pending-button preview shows both button variants in every state.
Pass an optional successIcon to customize the completion glyph while retaining the shared pending and error feedback. Catalog Reset and Copy actions use icon-only buttons with tooltips and a double check on completion; the icon-action preview demonstrates the same option. Accessible state labels remain required.
const action = useAsyncAction();
<PendingButton
status={action.status}
pendingLabel="Saving…"
successLabel="Saved"
errorLabel="Try again"
onClick={() => action.run(saveChanges)}
>
Save changes
</PendingButton>;
useAsyncAction is implemented by @nintu/creator-core-runtime/use-async-action; @/hooks/use-async-action re-exports it for existing app imports. It prevents concurrent duplicate calls, handles thrown errors and service result errors, and only reports success after the real operation succeeds. run(action, successMessage?) resolves to a boolean: apply local follow-up changes only when it returns true. Error details remain available through error; display them beside the affected action with retry guidance. A success indicator returns to idle after the shared feedback duration. Failures remain visible until the next action or an explicit reset.
For switches and other controls, use PendingIndicator with the same operation status. Its label supplies an accessible description; showLabel also displays that wording. Disable the affected control while its update is pending, preserve its value until success unless the feature already implements a reliable optimistic update, and restore it on failure. Do not disable unrelated navigation or replace the whole page.
The registered Toaster also uses PendingIndicator for loading and promise toasts. Its icon is decorative because Sonner owns the toast announcements. Use this shared wrapper for toast feedback; the toast gallery includes locally simulated success and failure promises.
Existing account forms and confirmations use useAccountAction, which adopts this shared lifecycle. Continue using the registered SaveForm, ActionButton, and ConfirmAction wrappers. Connected components keep their service calls, permission checks, and callbacks. Do not introduce an alternate auth client, mock provider, or delay around OAuth to implement loading feedback. Hexclave's hosted flows and Stripe's secure payment surfaces keep their existing ownership.
Coordinated page motion
Use the registered primitives from @/components/motion/primitives for new application entrances and animated content changes. They share one JavaScript animation engine, motion/react, and the settings in @nintu/creator-core-runtime/motion (also available through src/lib/motion.ts):
| Component | Purpose |
|---|---|
PageEntrance | Establish the shared entrance sequence for a page or section. |
Reveal | Reveal a section with the shared fade and lift; order controls its place in the sequence. |
RevealText | Reveal heading words using the shared text treatment; choose the semantic element with as. |
ContentReveal | Move from an unresolved fallback to ready content without waiting for an exit animation. |
MotionList | Animate keyed items entering, leaving, and changing position. |
InteractiveSurface | Add subtle pointer and hover feedback to a surface. |
Keep the sequence short and bounded. Group content by meaning—heading, summary cards, main content—instead of making every control a separate animation. Use the shared order mechanism rather than adding page-specific timers or per-item timing values. Long lists must not accumulate a delay proportional to their full length.
An entrance belongs to a mounted page or section. Keep its identity stable during saves, filtering, and background updates so the whole page does not replay. List item keys identify the data, not their current positions. Animate the affected list change while the rest of the interface remains available.
Row-owned portals use useMotionItemPresence() from the registered primitives. It returns null outside a motion row, otherwise { isPresent, getExitFocusTarget }. The shared ConfirmAction closes and disables its portaled dialog as soon as the row exits, then returns focus to a surviving row or the list. Reuse that wrapper for account confirmations. For another row-owned portal, use the same presence and focus target; inert on the list item alone cannot disable content portaled outside it. Keep normal cancellation focus restoration and do not reopen a dialog if a removed ID returns during its exit.
Pass ContentReveal a registered ContentSkeleton fallback only while content is unresolved. As soon as data is ready, render it; do not add a minimum loading time or wait for a skeleton animation to finish. Its entrance runs on mounting the ready content, not on ordinary updates to that content. During a refresh, retain already resolved content and keep ready true within the same user/team context. Reset the data and region identity when that context changes. Readiness and service actions remain the responsibility of the connected component; motion does not establish authentication or authorize data access.
InteractiveSurface supplies visual feedback; the workspace overview uses it on its two summary cards. Keep links, buttons, focus handling, and accessible names in the existing native controls. Text animation must preserve a single readable heading rather than announcing every animated word separately. Preserve enabled animations; this project does not add system reduced-motion overrides.
The complete composition is available at /components/motion-dashboard, with focused examples at /components/motion-primitives. Its reusable view lives in src/components/dashboard/motion-dashboard.tsx; all example records, simulated requests, and replay controls live in the gallery. The homepage, workspace overview, settings, Brand, component catalog/details, icon browser, and recovery pages use the same baseline. Page identity remains stable during data updates and filtering; only a changed route, account, or settings section starts a new entrance. The persistent navigation shell does not replay with page content. Marketing and resource routes use the corresponding PublicShell variant; Docs and Components retain only local section navigation, while Brand and Icons use the resource frame directly. The connected AppShell remains within /app. Keep resource loading, error, and not-found feedback inside the shared resource layout so its site navigation remains available.
Animated values and progress
Use AnimatedNumber from @/components/motion/animated-number for prominent metrics such as a dashboard total, currency amount, or percentage. Keep ordinary numbers in body copy, identifiers, and dense tables readable without animating every value. The component shares its animation lifecycle with Progress through @nintu/creator-core-runtime/use-animated-value and motionTransitions.value. The existing src/hooks/use-animated-value.ts entry re-exports that hook.
Pass a numeric value, with format as Intl.NumberFormatOptions and an explicit locale when needed; defaults are integer formatting and locale en-US. Set fraction digits explicitly for decimal metrics. prefix and suffix add surrounding text, order places the initial animation in the bounded sequence, and regular span props such as className remain available. For example:
<AnimatedNumber
value={12480}
format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
order={2}
/>
The component reserves the formatted target's width and exposes that target once to assistive technology. The changing visual number is decorative.
The initial animation starts once the value enters view. Later increases and decreases retarget from the current displayed value, including when a second update arrives before the first animation finishes. The real resolved value remains authoritative; animation does not delay a service result or declare an action complete. Keep stable component identities for updates so values do not restart from zero on each render.
Use the registered Progress and its ProgressValue together when a number describes a bar. Both read the same animated value, keeping the displayed count or percentage synchronized with the fill. Do not put an independently animated number beside the bar for that same value. Base UI's progress semantics expose the actual target immediately; decorative animation frames must not create repeated screen-reader announcements.
ProgressValue displays a percentage by default. Use display="fraction" for a completed count and total. Progress accepts the shared order for its initial entrance and preserves Base UI's root format and locale props:
<Progress value={4} max={5} order={6}>
<ProgressLabel>Create something</ProgressLabel>
<ProgressValue display="fraction" />
</Progress>
The standard value display stays synchronized automatically. An explicit ProgressValue children callback remains supported, but that callback owns its custom formatting and animation. Null or nonfinite progress values hide the fill; they do not simulate a percentage.
Progress requires a real measured value and range. Do not turn an unknown-duration request into an invented percentage or automatically increasing bar. Incoming content still uses a skeleton; an unknown-duration user action still uses the shared pending controls.
The standalone examples are /components/animated-number and /components/progress; /components/motion-dashboard shows them in context. The shared Progress implementation updates existing consumers automatically. The dashboard remains a representative fixture composition; application pages compose the same primitives around their own real content.
Scroll indicators
The root Providers mounts useScrollActivity from src/hooks/use-scroll-activity.ts once. It tracks the actual scrolling document or element, including nested regions and portaled menus or sheets, and marks only that region with data-scroll-active. Global native scrollbar styles use transparent tracks and a subtle semantic thumb color in both themes. WebKit scrollbar styling uses a 4 px indicator; other supporting browsers use their native thin width. Width stays constant while the thumb hides, so scrolling does not change the content layout.
After 700 ms without a scroll event, the indicator hides with the shared standard transition where the browser supports scrollbar color transitions. The idle delay uses scrollIdle in src/lib/motion.ts; the fade uses the existing standard token. Scrolling keeps native wheel, trackpad, touch, and keyboard behavior. Hover alone does not reveal an indicator. Keep this behavior global instead of adding independent scroll timers or listeners to each component.
The /components/app-shell preview includes a named, focusable vertical scrolling region with a nested horizontal region. Verify that scrolling either region reveals its own indicator, inactivity hides it, keyboard arrows still scroll focused regions, and both themes remain legible. Also check the document and a portaled scrollable control.
One place to tune motion
packages/runtime/src/motion.ts is the source for motion durations, easing, distance, and the entrance/list/value presets consumed by the registered components. Its package export is @nintu/creator-core-runtime/motion; src/lib/motion.ts is a compatibility re-export. The root layout exposes the CSS values. src/app/globals.css imports @nintu/creator-core-runtime/styles.css, whose shared classes live in packages/runtime/src/styles.css; stylesheet defaults are generated from motionVariables during the runtime build. The loading/action settings are:
| Token | Default | Purpose |
|---|---|---|
fast | 120 ms | Dialogs, menus, popovers, tooltips, and toast controls |
standard | 180 ms | Ordinary control transitions, action feedback, sheet backdrops, and toasts |
slow | 240 ms | Sheet panels, sidebar movement, and accordions |
feedback | 1600 ms | Successful action feedback before returning to idle |
skeleton | 1600 ms | Skeleton animation cycle |
spinner | 800 ms | Pending indicator cycle |
scrollIdle | 700 ms | Scroll inactivity before hiding the indicator |
distance | 3 px | Small state transition movement |
Page motion adds these settings:
| Token | Default | Purpose |
|---|---|---|
entrance | 750 ms | Section and heading-word entrance duration |
surface | 420 ms | Content reveals, list changes, and surface feedback |
value | 900 ms | Metric and progress-value animation |
stagger | 75 ms | Gap between section orders |
maxStagger | 300 ms | Maximum total entrance delay, including words |
textStagger | 38 ms | Gap between heading words |
entranceDistance | 12 px | Section entrance lift |
textDistance | 8 px | Heading-word entrance lift |
textBlur | 2 px | Starting blur for heading words |
spotlightRadius | 260 px | Pointer-light radius on interactive surfaces |
entranceDelay caps the combined section/word delay, so section and text entrances finish within 1050 ms at the defaults even with many children. motionTransitions.entrance and motionTransitions.surface use the shared [0.22, 1, 0.36, 1] ease-out. motionTransitions.value uses [0.4, 0, 0.2, 1] ease-in-out so counts and fills accelerate gently and remain visibly active for more of the transition. Both curves live in motion.ts. The smaller lift and blur keep movement restrained. The separate surface token preserves existing control durations. Initial list items appear together within their containing section's entrance; subsequent insertions, removals, and reordering animate locally.
Use the provided motion classes, presets, and components. Keep timing conversion for Motion in the runtime motion module; consumers should not copy numeric durations or easing curves. Do not introduce independent loading keyframes, spinner speeds, or animate-spin/animate-pulse treatments in application UI. These defaults use the existing semantic colors and work in both themes. Content appears as soon as it is ready; the system adds no artificial minimum loading time or delayed success.
The package owns shared behavior: motion settings, animated values, action state, and provider-independent result/error handling. Visual components remain editable source under src/components, with their existing catalog entries and previews. Package hook entries preserve "use client"; the motion entry remains safe for server layouts. React 19 and Motion 12 are peer dependencies, so consumers use their own installed instances. In the Creator Core maintenance repository, build the runtime with bun run --cwd packages/runtime build before app checks/builds. Derived products consume its released ESM and declarations; they update the package dependency instead of copying or editing shared behavior locally. Import the package stylesheet once for loading styles and default variables. Product palettes and native scroll styling remain in the app stylesheet.
Optional React Bits effects are installed directly into a derived product under its developer's license. They are not bundled with this template. Use the registered primitives for new owned application motion; installing an authored effect does not integrate it into the shared motion system automatically.
The shared settings drive skeletons, action feedback, scroll indicators, page/content/list motion, AnimatedNumber, Progress, and native control/overlay transitions. globals.css maps Tailwind's default transition duration/easing to standard and --motion-ease; explicit speeds use duration-(--motion-fast) or duration-(--motion-slow) with ease-(--motion-ease). Keep Base UI responsible for open/close, focus, gestures, and semantics. CSS transitions remain appropriate for native controls; do not wrap every control in a Motion component.
The Toaster uses the runtime's scoped motion-toaster styles. Toast motion uses standard and its controls use fast, within Sonner's installed removal deadline. Preserve gesture tracking and the provider's lifecycle. tests/motion-tokens.test.ts checks the compiled CSS and deadline so a future token/package change cannot silently truncate exits.
Notes, sessions, API keys, security accounts, and team records use stable IDs with MotionList. Keep an empty list mounted through the final row's exit so focus has a destination. Catalog plans also have stable IDs; billing purchase/invoice results without unique instance IDs keep their existing row identity and reveal the resolved region/page instead of inventing list keys. Account data remains keyed by its user/team boundary, and invoice pagination retains the prior page until its next result is ready.
Preview and verify
The Loading & motion collection includes /components/skeleton, /components/content-skeleton, /components/pending-button, /components/pending-indicator, /components/motion-primitives, /components/animated-number, and /components/motion-dashboard. The shared progress preview is at /components/progress. The examples import the real components and exercise incoming layouts, fast and slow responses, success, failure, retries, icon actions, coordinated entrances, list changes, and animated values. Simulated service timing stays under src/components/gallery; it never reaches production service code.
For each new flow, verify the initial skeleton, visible content during an update, duplicate submission prevention, error and retry behaviour, keyboard interaction, accessible status text, and a stable footprint in both themes. For entrance or list changes, also verify bounded staggering, stable content during refresh, focus through an update, narrow screens, and that unrelated interactions do not replay the page. For animated metrics and progress, verify increases, decreases, a second update during animation, formatting, synchronized bar/label values, and truthful accessible targets. Run bun run check:components, then the normal checks and Cloudflare build before pushing.
The component check rejects ad hoc loader icons, pulse/spin classes, visible standalone loading text, and conditional pending labels in plain buttons throughout owned application components, native UI controls, and app routes. New components are covered automatically. Only PendingIndicator owns the loader icon. Accessible labels remain supported. Gallery code may contain local presentation fixtures; adding a registry field does not exempt a component. Review still needs to verify that the chosen layout and scope match the operation.
Motion enforcement runs through scripts/motion-policy.ts as part of check:components. It scans owned TS/TSX and CSS, including gallery code, for independent duration/easing values, animation engines, browser animation lifecycles, and ad hoc keyframes. The registered primitive/value/progress implementations are the explicit engine owners; shared keyframes live in the runtime. Native animate-in, animate-out, and accordion utilities remain supported with shared timing. An animation is not required on every element: ordinary numbers, dense tables, static menu options, and the persistent shell stay readable and stable.