6.6 KiB
@langgenius/dify-ui
Shared UI primitives, design tokens, Tailwind preset, and the cn() utility consumed by Dify's web/ app.
The primitives are thin, opinionated wrappers around Base UI headless components, styled with cva + cn and Dify design tokens.
private: true— this package is consumed byweb/via the pnpm workspace and is not published to npm. Treat the API as internal to Dify, but stable within the workspace.
Installation
Already wired as a workspace dependency in web/package.json. Nothing to install.
For a new workspace consumer, add:
{
"dependencies": {
"@langgenius/dify-ui": "workspace:*"
}
}
Imports
Always import from a subpath export — there is no barrel:
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import '@langgenius/dify-ui/styles.css' // once, in the app root
Importing from @langgenius/dify-ui (no subpath) is intentionally not supported — it keeps tree-shaking trivial and makes Storybook / test coverage attribution per-primitive.
Primitives
| Category | Subpath | Notes |
|---|---|---|
| Overlay | ./alert-dialog, ./context-menu, ./dialog, ./dropdown-menu, ./popover, ./select, ./toast, ./tooltip |
Portalled. See Overlay & portal contract below. |
| Form | ./number-field, ./slider, ./switch |
Controlled / uncontrolled per Base UI defaults. |
| Layout | ./scroll-area |
Custom-styled scrollbar over the host viewport. |
| Media | ./avatar, ./button |
Button exposes cva variants. |
Utilities:
./cn—clsx+tailwind-mergewrapper. Use this for conditional class composition../tailwind-preset— Tailwind v4 preset with Dify tokens. Apps extend it from their owntailwind.config.ts../styles.css— the one CSS entry that ships the design tokens, theme variables, and base reset. Import it once from the app root.
Overlay & portal contract
All overlay primitives (dialog, alert-dialog, popover, dropdown-menu, context-menu, select, tooltip, toast) render their content inside a Base UI Portal attached to document.body. This is the Base UI default — see the upstream Portals docs for the underlying behavior. Consumers do not need to wrap anything in a portal manually.
Root isolation requirement
The host app must establish an isolated stacking context at its root so the portalled overlay layer is not clipped or re-ordered by ancestor transform / filter / contain styles. In the Dify web app this is done in web/app/layout.tsx:
<body>
<div className="isolate h-full">{children}</div>
</body>
Equivalent: any root element with isolation: isolate in CSS. Without it, overlays can be visually clipped on Safari when a descendant creates a new stacking context.
z-index layering
Every overlay primitive uses a single, shared z-index. Do not override it at call sites.
| Layer | z-index | Where |
|---|---|---|
| Overlays (Dialog, AlertDialog, Popover, DropdownMenu, ContextMenu, Select, Tooltip) | z-1002 |
Positioner / Backdrop |
| Toast viewport | z-1003 |
One layer above overlays so notifications are never hidden under a dialog. |
Rationale: during Dify's migration from legacy portal-to-follow-elem / base/modal / base/dialog overlays to this package, new and old overlays coexist in the DOM. z-1002 sits above any common legacy layer, eliminating per-call-site z-index hacks. Among themselves, new primitives share the same z-index and rely on DOM order for stacking — the portal mounted later wins.
See [web/docs/overlay-migration.md](../../web/docs/overlay-migration.md) for the Dify-web migration history and the remaining legacy allowlist. Once the legacy overlays are gone, the values in this table can drop back to z-50 / z-51.
Rules
- Never add
z-1003/z-9999/ etc. overrides on primitives from this package. If something is getting clipped, the parent overlay (typically a legacy one) is the problem and should be migrated. - Never portal an overlay manually on top of our primitives — use
DialogTrigger,PopoverTrigger, etc. Base UI handles focus management, scroll-locking, and dismissal. - When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it inside the exported component, not at call sites.
Development
pnpm -C packages/dify-ui test— Vitest unit tests for primitives.pnpm -C packages/dify-ui storybook— Storybook on the default port. Each primitive hasindex.stories.tsx.pnpm -C packages/dify-ui type-check—tsc --noEmitfor this package only.
See [AGENTS.md](./AGENTS.md) for:
- Component authoring rules (one-component-per-folder,
cva+cn, relative imports inside the package, subpath imports from consumers). - Figma
--radius/* token → Tailwindrounded-*class mapping.
Not part of this package
- Application state (
jotai,zustand), data fetching (ky,@tanstack/react-query,@orpc/*), i18n (next-i18next/react-i18next), and routing (next) all live inweb/. This package has zero dependencies on them and must stay that way so it can eventually be consumed by other apps or extracted. - Business components (chat, workflow, dataset views, etc.). Those belong in
web/app/components/....