Files

@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 by web/ 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:

  • ./cnclsx + tailwind-merge wrapper. Use this for conditional class composition.
  • ./tailwind-preset — Tailwind v4 preset with Dify tokens. Apps extend it from their own tailwind.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 has index.stories.tsx.
  • pnpm -C packages/dify-ui type-checktsc --noEmit for 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 → Tailwind rounded-* 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 in web/. 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/....