Troubleshoot
Troubleshoot
A decision tree for the most common shilp-sutra setup breakages. Read symptoms top-to-bottom; the first match is usually the right diagnosis.
Symptom: Tailwind utilities don't apply (no styling at all)
Diagnosis: Tailwind is not detecting design-system source classes, OR the CSS imports are out of order.
Check 1 — import order. Open the global CSS file. The order MUST be:
@import "tailwindcss"; /* FIRST */
@import "@devalok/shilp-sutra/css"; /* SECOND */If reversed, swap them.
Check 2 — both imports present. Some setups accidentally drop @import "tailwindcss" after a refactor. Both imports are required.
Check 3 — file is actually loaded. In Next.js, globals.css must be imported from app/layout.tsx (App Router) or pages/_app.tsx (Pages Router). In Vite, from src/main.tsx. In Remix, via links export. In Astro, from a layout file.
Symptom: Spacing utilities like p-4 don't work but p-ds-04 does
Diagnosis: Working as designed.
Shilp Sutra uses the --spacing-ds-* namespace to avoid colliding with consumer numeric spacing. Use p-ds-04, gap-ds-03, mx-ds-08, etc. Plain p-4 is the consumer's own spacing (Tailwind's default scale) — it works but is unrelated to the design system.
If p-ds-* does NOT work, you have the styling-not-applying issue above.
Symptom: Console error or weird animation glitches involving MotionConfig, LayoutGroup, or AnimatePresence
Diagnosis: Multiple copies of framer-motion are resolved.
Run:
pnpm why framer-motion
# or
npm ls framer-motionIf more than one version is listed, fix with overrides:
pnpm:
// package.json
{
"pnpm": {
"overrides": {
"framer-motion": "^12"
}
}
}npm:
// package.json
{
"overrides": {
"framer-motion": "^12"
}
}yarn (berry):
// package.json
{
"resolutions": {
"framer-motion": "^12"
}
}bun:
// package.json
{
"overrides": {
"framer-motion": "^12"
}
}After editing, delete the lockfile + node_modules and reinstall.
Symptom: Next.js error: Cannot find module '@devalok/shilp-sutra/...' or Module parse failed: Unexpected token
Diagnosis: transpilePackages is missing from next.config.{ts,js,mjs}.
Add:
<!-- typecheck-skip: a fragment of next.config, not a standalone module -->transpilePackages: ["@devalok/shilp-sutra"],Symptom: Build error Cannot find module 'sonner' / 'input-otp' / 'date-fns' / 'react-pdf' / 'react-markdown' — OR (on Vite 8) a runtime Could not resolve "…" from a green build
Diagnosis: an optional peer dependency is missing. Each component below has a peer it pulls only when imported. Install the matching peer (always BEFORE the first import). On Vite 8 / Rolldown this does not fail the build — it throws at runtime — so run the MCP verify_setup tool to catch it early.
| You imported (per-component subpath) | Install |
|---|---|
…/ui/toaster or …/ui/toast | pnpm add sonner |
…/ui/input-otp | pnpm add input-otp |
…/composed/date-picker or …/composed/schedule-view | pnpm add date-fns |
…/ui/data-table or …/ui/data-table-toolbar | pnpm add @tanstack/react-table @tanstack/react-virtual |
…/composed/file-preview | pnpm add react-pdf react-zoom-pan-pinch |
…/composed/markdown-viewer | pnpm add react-markdown react-syntax-highlighter remark-gfm |
…/ai/block-renderer, …/ai/blocks/text, …/ai/blocks/error | pnpm add react-markdown remark-gfm |
Any …/ui/charts/* | pnpm add d3-axis d3-scale d3-selection d3-shape |
These ship as optional peers so consumers who never render the matching component don't pay the install cost. Once you import the component, the peer becomes required. Each affected component's JSDoc carries the same install hint — hover the import in your editor to see it inline.
No longer peers (bundled since the frimousse migration): the emoji picker (…/composed/emoji-picker) and the rich-text editors (…/composed/rich-text-editor, …/composed/rich-chat-input) bundle their dependencies (frimousse, @emoji-mart/data, TipTap) into a lazy chunk — you do not install anything for them. @tabler/icons-react is a required peer that most package managers auto-install.
Catch this at edit time, not build time: install @devalok/eslint-plugin-shilp-sutra (pnpm add -D @devalok/eslint-plugin-shilp-sutra, then shilpSutra.configs['flat/recommended']). Its prefer-per-component-import rule flags peer-cliff symbols imported from a barrel and autofixes the path — surfacing the cliff in your editor before the bundler ever fails.
For the full table in your framework's install recipe, see install-<framework>.md → §2a. Optional peer dependencies.
Symptom: Hydration warning on every page load (Next.js)
Diagnosis: next-themes writes the class attribute on <html> before React hydrates, causing a server/client class mismatch.
Add suppressHydrationWarning to <html> in app/layout.tsx (App Router) or pages/_document.tsx (Pages Router):
<html lang="en" suppressHydrationWarning>This warning is specific to the class attribute on <html> — it does NOT suppress hydration warnings on other elements.
Symptom: Dark mode toggle does nothing
Diagnosis: The .dark class is not being applied to <html> (or any ancestor of the components).
Quick verification — open the browser console and run:
document.documentElement.classList.add("dark");If components now render in dark mode, the toggle wiring is broken (not the design system). Check:
next-themesis installed and<ThemeProvider attribute="class">wraps the app- For Vite/Remix/Astro/TanStack: the
theme-bootstrap.jsscript runs before any React mount
If .dark IS on <html> and components still look light, the CSS imports are out of order — see the first symptom above.
Symptom: RSC error — You're importing a component that needs useState. It only works in a Client Component
Diagnosis: A client-only shilp-sutra component is being imported into a Server Component via the barrel.
Switch to per-component imports:
// ❌ pulls client code into RSC
import { Button } from "@devalok/shilp-sutra/ui";
// ✅ component-scoped, declares "use client" only where needed
import { Button } from "@devalok/shilp-sutra/ui/button";For the full RSC-safety matrix, see server-components.md.
Symptom: Fonts render in browser default (Times/Arial), not Inter/Ranade
Diagnosis: Either the CSS import did not load (see first symptom), or the consumer is overriding --font-sans / --font-display and pointing at a font that isn't loaded.
Check the computed value of --font-sans on <html> in DevTools. It should be "Inter Variable", system-ui, .... If the override variable points at a font that isn't loaded, the browser falls back.
The font files ship inside the package — no next/font configuration is required for the defaults to work.
Symptom: Bare shadow class produces no visible shadow
Diagnosis: Working as designed. Tailwind 4 has no default --shadow token, so the shadow utility no longer exists.
Use the explicit shadow variants:
shadow-raised— cards, buttonsshadow-overlay— popovers, dropdownsshadow-floating— modals, dialogsshadow-brand— accent emphasis
Symptom: <Toaster /> is mounted but toast() doesn't show anything
Diagnosis: Either sonner is not installed, or two <Toaster /> instances are mounted at different positions and they're stacking off-screen.
Check 1 — pnpm list sonner shows ^2.0.0 or higher.
Check 2 — only ONE <Toaster /> is mounted in the app. Search the project: grep -r "<Toaster" src/ app/.
In dev mode, calling toast() without a mounted <Toaster /> logs a one-time console warning pointing to the fix (since v0.36.0).
Symptom: Build error mentioning use-sync-external-store
Diagnosis: we no longer depend on it at all, as of v0.56.0. Nothing in our dist imports it.
It was never ours: React 18+ has useSyncExternalStore built in and our own code calls it directly. The dependency existed purely because we bundled TipTap, and that bundled chunk imported the shim. TipTap is now externalized, so the shim went with it.
If you still see the error, something else in your tree wants it — find out what and install it there:
pnpm why use-sync-external-storeIf the trail leads back to @devalok/shilp-sutra, that's a bug: open an issue at https://github.com/devalok-design/shilp-sutra/issues with that output.
Symptom: Cannot find module '@tiptap/core' (or another @tiptap/*) after upgrading to 0.56.0
Diagnosis: expected, and it is the one breaking change in 0.56.0. We used to bundle TipTap and list it as an optional peer — so RichTextEditor / RichChatInput worked whether or not you installed the peers. It is now externalized, so you must install them.
Why we changed it: a consumer who did follow the peer instructions ended up running two ProseMirror copies. Plugin keys are module-scoped, so the two copies could not see each other's plugins and the editor misbehaved in ways that were near-impossible to debug. It also cut 641 KB from the package for everyone who never touches rich text.
Fix — install the peers for the component you import (§2a of your framework's install recipe has the exact line):
# RichTextEditor
pnpm add @tiptap/core @tiptap/extension-highlight @tiptap/extension-image \
@tiptap/extension-list @tiptap/extension-mention @tiptap/extension-text-align \
@tiptap/extensions @tiptap/markdown @tiptap/pm @tiptap/react \
@tiptap/starter-kit @tiptap/suggestion
# RichChatInput — the same list plus date-fns
pnpm add date-fnsOr ask the docs MCP: preflight reports exactly which peers your imports need.
Symptom: Cannot find package 'sonner' when you only imported a hook (or 'react-markdown' from the AI barrel)
Diagnosis: you imported a barrel rather than the component itself, and the barrel re-exports something with an optional peer.
@devalok/shilp-sutra/hooksre-exportstoast, which needssonner.@devalok/shilp-sutra/aire-exports the block renderer, which needsreact-markdown.
A bundler tree-shakes the unused branch away, so this is invisible in a client build. It bites at runtime in Node — SSR, a route handler, a test — where the import is evaluated for real.
Fix — import the specific module instead of the barrel:
// needs sonner installed, because the barrel also exports toast
import { useIsMobile } from '@devalok/shilp-sutra/hooks'
// no optional peers at all
import { useIsMobile } from '@devalok/shilp-sutra/hooks/use-mobile'Every hook has its own subpath: use-mobile, use-color-mode, use-touch-device, use-viewport-height. Same rule applies across the library — a deep import never costs you more than that component needs.
Symptom: error TS2305: Module '"react"' has no exported member 'ReactSVG'
This is an upstream @tabler/icons-react bug, not a shilp-sutra one — but you will hit it following our install instructions, so it is documented here.
@tabler/icons-react (through 3.45.0, the current release) opens its type declarations with:
import { ReactSVG, … } from 'react'React 18's types exported ReactSVG; React 19's removed it, keeping only ReactSVGElement. So on React 19 the icon package's own .d.ts fails to compile.
You only see it with all three: React 19, skipLibCheck: false, and a direct @tabler/icons-react import in your own code. React 18 is unaffected, and the default skipLibCheck: true suppresses it.
Workarounds, in order of preference:
- Leave
skipLibCheck: true(the default in every framework scaffold). It suppresses errors inside dependency declarations — including this one. - Import icons through our re-export instead of directly, so the broken declaration is never loaded into your program.
- Pin
@types/reactto 18 if your app is still on React 18.
There is nothing to fix on our side: we neither wrap nor re-declare Tabler's types. Track it upstream at https://github.com/tabler/tabler-icons/issues.
Symptom: error TS2307: Cannot find module '@devalok/shilp-sutra/ui/button' under "moduleResolution": "node"
Diagnosis: You are on TypeScript's legacy resolution mode, which predates and ignores the exports field in package.json. Our files live under dist/, and every public path is mapped through exports — legacy resolution looks for a literal node_modules/@devalok/shilp-sutra/ui/button.js and finds nothing.
Supported resolution modes:
moduleResolution | Supported | Notes |
|---|---|---|
bundler | ✅ | Recommended. Vite, Next.js, and most modern setups default to this. |
node16 / nodenext | ✅ | Fully supported since 0.55.0. |
node (legacy) | ❌ | Cannot read exports. No subpath resolves. |
Fix — in your tsconfig.json:
{ "compilerOptions": { "moduleResolution": "bundler" } }Symptom: require('@devalok/shilp-sutra') throws ERR_REQUIRE_ESM
Diagnosis: The package is ESM-only — we ship no CommonJS build. Whether require() works depends on your Node version:
| Node | require('@devalok/shilp-sutra/ui/button') |
|---|---|
| 22.12+ / 23+ | ✅ Works — require(esm) is supported and unflagged. Verified against every entry point, including the full ./ui barrel. |
| < 22.12 | ❌ ERR_REQUIRE_ESM |
On older Node, use a dynamic import:
const { Button } = await import('@devalok/shilp-sutra/ui/button')Or move the consuming file to ESM ("type": "module", or a .mjs extension). Every supported framework target — Next.js, Vite, Remix, Astro, TanStack Start — handles the ESM entry natively; this only affects hand-written CJS scripts.
Symptom: Storybook MCP server localhost:6006/mcp returns 404
Diagnosis: Storybook dev server isn't running, OR the MCP plugin is not enabled in this Storybook version.
Check 1 — pnpm dev is running and http://localhost:6006/ shows the Storybook UI.
Check 2 — the MCP endpoint requires Storybook 9+ with the MCP plugin enabled. Older versions of this repo's Storybook setup may need an upgrade.
The MCP server is a development convenience for AI agents — llms.txt, the per-component docs at docs/components/, and mcp-manifest.json are the authoritative shipped docs and do not require a running server.
Still stuck
Open an issue with this template:
## Environment
- Framework + version (Next.js 15.x / Vite 5.x / Remix 2.x / Astro 5.x / TanStack Start ...)
- Package manager + version (pnpm 10.x / npm 11.x / yarn / bun)
- Node version
- @devalok/shilp-sutra version
- Other peer deps (framer-motion, sonner, tailwindcss versions)
## What I tried
[Recipe followed, step where it broke]
## What happened
[Exact error message, stack trace, screenshot]
## What I expected
[Behavior described in the recipe]File at: https://github.com/devalok-design/shilp-sutra/issues/new