dify/packages/dify-ui
2026-05-01 02:39:49 +08:00
..
.storybook chore: migrate to tailwind css first config 2026-05-01 02:39:49 +08:00
src chore: migrate to tailwind css first config 2026-05-01 02:39:49 +08:00
.gitignore refactor(dify-ui): finish primitive migration from web/base/ui to @langgenius/dify-ui (#35349) 2026-04-17 08:46:11 +00:00
AGENTS.md chore: migrate to tailwind css first config 2026-05-01 02:39:49 +08:00
package.json chore: migrate to tailwind css first config 2026-05-01 02:39:49 +08:00
README.md chore: migrate to tailwind css first config 2026-05-01 02:39:49 +08:00
tsconfig.json chore: migrate to tailwind css first config 2026-05-01 02:39:49 +08:00
vite.config.ts test(dify-ui): disable base ui animations globally (#35467) 2026-04-24 08:12:23 +00:00
vitest.setup.ts test(dify-ui): disable base ui animations globally (#35467) 2026-04-24 08:12:23 +00:00

@langgenius/dify-ui

Shared UI primitives, design tokens, CSS-first Tailwind styles, 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.
  • ./styles.css — the one CSS entry that ships the design tokens, theme variables, and base reset. Import it once from the app root.

Tailwind CSS v4 integration

This package uses Tailwind CSS v4's CSS-first configuration model. Consumers should import Tailwind from their own root stylesheet, then import this package's CSS entry:

@import 'tailwindcss';
@import '@langgenius/dify-ui/styles.css';

If a consumer uses Dify UI source files through the workspace, add an explicit source so Tailwind can detect utility classes:

@source '../packages/dify-ui/src';

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-checktsgo --noEmit for this package only.

Disabling Animations In Tests

Base UI can wait for element.getAnimations() to finish before it unmounts overlays, panels, and transition-driven components. Browser-based test runners can make that timing unstable, especially when tests assert final DOM state rather than animation behavior.

Set the Base UI test flag in a Vitest setup file to skip those waits:

(
  globalThis as typeof globalThis & {
    BASE_UI_ANIMATIONS_DISABLED: boolean
  }
).BASE_UI_ANIMATIONS_DISABLED = true

packages/dify-ui/vitest.setup.ts already applies this for primitive tests.

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/....