@eldrajs/ui

中文文档
0.3.6发布于 2026/11/12MITTypeScriptES Module构建来源已验证近 30 天下载 3656

Accessible Vue 3 core components for Eldra storefronts: buttons, forms, selects, cards, overlays and navigation, themed through CSS design tokens.

原始使用文档
文档目录7 节
  1. Install
  2. Styles
  3. Overriding a component's classes
  4. The data-eldra-ui marker
  5. Theming a subtree
  6. Fonts
  7. Customisation

@eldrajs/ui

Accessible Vue 3 core components for Eldra storefronts, themed through CSS design tokens.

The components are built to eldra-starter-spec/01-core-components.md (WCAG 2.2 AA). Plan 1 — foundations, actions and forms (Button/ButtonGroup, Link, Input, Textarea, FieldWrapper, FormLayout, Checkbox/CheckboxGroup, Radio/RadioGroup, Switch, Select/MultiSelect, QuantityStepper, VariantPicker, SearchBar) — is complete, plus UnitInput and CurrencyInput, which the spec has no equivalent of; display, commerce, layout, overlays, navigation and feedback components land in the two sub-projects that follow, in the same delivery order the spec's "Components and delivery order" section lays out. What this package ships that the spec does not name is listed under Additions beyond the spec, and where it departs from a property table it is recorded under Deviations — neither is left for a reader to find.

Install

pnpm add @eldrajs/ui vue

Peer: vue ^3.5. The floor is 3.5, not 3.4: every control's ids come from useId(), which Vue added in 3.5, so on 3.4 the install succeeds and the first control to mount throws useId is not a function. vee-validate ^4.12 is an optional peer — only the ./vee-validate entry needs it, and nothing else in the package imports it.

Styles

The components come prebuilt: an app never compiles this package's styles and needs no Tailwind of its own to use them. Every class a component renders is eldra:-prefixed (eldra:px-4, eldra:hover:bg-primary), compiled with Tailwind's prefix(eldra), and every theme variable it reads is --eldra-*. Nothing in the package's CSS uses a name an app's own Tailwind build uses, so the two never restyle each other's markup.

Five CSS entries ship. Every one starts from the same --eldra-* variables, so none is a different theme, only a different delivery shape.

  • @eldrajs/ui/style.css — the tokens, Tailwind's preflight and every component's styles, for a page built from this package:

    @import '@eldrajs/ui/style.css';
    
  • @eldrajs/ui/components.css — style.css without Tailwind's preflight, for a site that has its own stylesheet (or its own Tailwind) and adds components to it:

    @import '@eldrajs/ui/components.css';
    

    Preflight is a reset for the whole page: style.css sets every h1–h6 to the surrounding size and weight, every ul/ol to no bullets and no indent, every a to the surrounding colour with no underline, every p and heading to no margin, and every button to no border or fill. It sits in @layer base, so a site's own rules still win where they exist, but anything the site left to the browser — rich text from the CMS, most often — loses its defaults. components.css carries the same theme and the same utilities, byte for byte, and narrows the reset to the elements the components render themselves, so the site's own elements keep the browser's defaults and anything passed into a component's slot keeps the site's styling. Use style.css when the page is built from this package; use components.css when the page is the site's own. src/styles/__tests__/componentsCss.browser.spec.ts renders every component family under both, the overlays open, and requires identical pixels.

  • @eldrajs/ui/tokens.css — the --eldra-* variables only (colours, radii, spacing, type, motion, z-index, and the one opacity — --eldra-revalidating-opacity), declared on :root with the spec's defaults, plus a @media (prefers-reduced-motion: reduce) rule that zeroes every duration variable.

  • @eldrajs/ui/tailwind.css — for an app that runs Tailwind v4 and wants its own markup on the same design tokens: tokens.css, then a @theme block mapping Tailwind's namespaces onto those variables (--color-primary: var(--eldra-color-primary), --radius-md, --font-heading, --spacing-*, --shadow-*, --ease-*) and the package's custom utilities (control heights, the eldra-focus ring, the type styles, motion and layer utilities). It builds none of the components; import the prebuilt ones beside it. theme.css (the @theme block) and utilities.css (the utilities) ship on their own too.

    @import 'tailwindcss';
    @import '@eldrajs/ui/tailwind.css';
    @import '@eldrajs/ui/components.css';
    

    The type styles (text-body, text-h2, …) set longhands, never the font shorthand, and read their weight as var(--tw-font-weight, <token>), the declaration Tailwind writes for its own --text-*--font-weight. So a font-medium/font-semibold beside one wins whatever order the two compile in, and a tabular-nums beside one is no longer reset. A base-layer @apply font-* sets --tw-font-weight as well, so it reaches a type style on the same element. Line height and tracking are plain longhands on purpose: the starter's own base rule (h1–h4 @apply leading-[1.1] tracking-tight) would otherwise override every heading's type style.

Overriding a component's classes

The components' styles sit in @layer components, below Tailwind's @layer utilities, and both style.css and components.css declare the standard order (properties, theme, base, components, utilities), so it holds whichever stylesheet loads first. A class you put on a component, with class (its outermost element) or a part of classes (an element inside it), is not prefixed and is added to the component's own classes; it wins by layer, whatever its specificity. Nothing is merged or removed at runtime: the components bind their classes the plain Vue way, one class per concern on each element.

<Button class="px-8">Checkout</Button>
<!-- renders class="eldra:control-h eldra:px-4.5 … px-8": px-8 wins -->
  • It wins in every state: a plain bg-red-600 on a Button replaces its hover and active fills too, because a utilities-layer rule beats any components-layer one. Write the states you want (bg-red-600 hover:bg-red-700).
  • Your app's own Tailwind must generate the class (it is in your source, so it does). Without a Tailwind of your own, any CSS of yours that is unlayered or in a layer declared after components wins the same way.

The data-eldra-ui marker

Every component's outermost element carries data-eldra-ui, and so does every element a component renders outside it: a Select or MultiSelect panel, a SearchBar results panel, a Popover panel, a Tooltip bubble and the Toaster region, each teleported out of its component. components.css's reset reaches exactly those elements and the data-part elements inside them, :where([data-eldra-ui], [data-eldra-ui] [data-part]), at zero specificity. data-part alone is not enough, because it is not this package's name: a site's own <h2 data-part="title">, or any Ark UI or Zag component (which put data-part on every element they render), keeps the browser's defaults. The attribute is public and stable, so a site can use it as a hook too (a:not([data-eldra-ui], [data-eldra-ui] *) keeps a site's global link rule out of the components). One case stays inside the scope: an element carrying its own data-part that the site passes into a component's slot (an Ark UI widget inside a Dialog, say) is a data-part element inside a marked root, so it gets the reset as well.

A site's unlayered element rules beat the package's layered ones regardless of specificity: a global a { color: … } or button { font: … } restyles those elements inside the components too. Scope such rules (.prose a) or put them in a layer.

Theming a subtree

provideEldraScope(name), called in the setup() of a wrapper or an app root, makes every component below it render data-eldra-scope="<name>" on its root and on every part it teleports (panels, bubbles, dialogs, toasts), so the --eldra-* variables can be set for those components alone, teleported parts included:

[data-eldra-scope='studio'] {
  --eldra-color-primary: #0b5cad;
}

Every element holding slot content you passed in also carries data-eldra-slot (a Button's label, a Dialog's title, body and footer, a Popover panel…), whenever the slot has content and never for a fallback, as a hook for your own CSS.

Fonts

The type tokens' defaults are --eldra-font-heading: "Bricolage Grotesque", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif and --eldra-font-body: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif. The package does not load either font — it only names them — so a consumer either loads Bricolage Grotesque and Instrument Sans themselves (self-hosted, or a Google Fonts <link>/@import) or overrides --eldra-font-heading and --eldra-font-body with their own stack. Until one or the other happens, text renders in the fallback system stack, which is legible but not the spec's intended look.

Customisation

Every component supports all five of these; none hard-codes anything a store might want to change.

  1. Tokens. Every colour, radius, height, spacing step, font, duration, easing and z-index a component uses resolves to a --eldra-* variable from Styles above — set one on any ancestor (typically :root) and every component reading it follows. :root rather than a wrapper, for the popup panels: Select, MultiSelect and SearchBar render theirs through a <Teleport> to body (see Layering), so a variable set on a <div> around the control is not an ancestor of the panel any more and is not inherited by it.

  2. Per-component CSS variables, declared on the component's own root with a token-derived default, for the handful of values that are not shared design tokens — a size with no dedicated token, a component-specific corner radius. Each one is documented on the component's own Storybook docs page; the variables currently declared:

    ComponentCSS variables
    Accordionnone — the top divider is a stock border-t, no per-component variable
    AccordionItem--eldra-accordion-title-line (default 1.4) — the label's line ratio, no token of its own
    Avatar--eldra-avatar-initials-size-{sm,md,lg,xl} (defaults 0.76rem/0.95rem/1.33rem/2.28rem, "38% of diameter" — no token of its own), --eldra-avatar-initials-tracking (default 0.02em), --eldra-avatar-icon-size-{sm,md,lg,xl} (defaults 0.857rem/1.071rem/1.5rem/2.571rem, the spec's own lg number scaled proportionally to the other three diameters)
    AvatarGroupnone — the overlap and stack order are plain Tailwind, no per-component variable
    Badge--eldra-badge-line-height (default 1) — the badge text's line ratio, no token of its own
    Breadcrumbnone — the collapse threshold reads --container-tablet (Container's own 48rem gutter breakpoint), and the link/ellipsis boxes reuse target-min/eldra-link-radius/radius-sm, no per-component variable of its own
    Button--eldra-button-radius (default var(--eldra-radius-md)), --eldra-button-border-width (default 1px; the height stays the same, and an attached ButtonGroup overlaps by this width), --eldra-button-line-height (default 1.2), --eldra-button-font-size-lg (default 1.0625rem, the one button size with no type token of its own)
    ButtonGroupnone — reads only the shared tokens from layer 1
    Carousel--eldra-carousel-per-view-base/-md/-lg (bound as an inline style on the track by carouselPerViewStyle, resolved into --eldra-carousel-per-view per container-query breakpoint by the eldra-carousel-track utility, no default of its own — a slide with none set stays full width) and --eldra-carousel-gap (default var(--eldra-space-4)), both read by the eldra-carousel-slide utility's width formula; --eldra-carousel-dot-size (default 0.5rem) and --eldra-carousel-dot-ring (default 1.5px), read by eldra-carousel-dot; --eldra-carousel-bleed (default 0px) — the inline gutter the track bleeds out to, as its own padding, the negative margin that cancels that padding and its scroll padding at once, each max()-ed with the focus ring's reach so a bleed never costs the ring the room the track reserves for it (set it per breakpoint through classes.track, never a px-*/-mx-*/scroll-px-* utility of your own — those replace the reservation on that axis instead of composing with it)
    Checkbox--eldra-checkbox-radius (default var(--eldra-radius-sm)), --eldra-checkbox-border-width (default 1.5px), --eldra-checkbox-border-width-invalid (default 2px)
    CheckboxGroupnone — reads only the shared tokens from layer 1
    Chipnone — the selected/hover fills are color-mix() over shared --eldra-color-* tokens, no per-component variable
    ChipGroupnone — reads only the shared tokens from layer 1
    Containernone — the gutter widths (--eldra-gutter-{mobile,tablet,desktop}) are shared tokens, also read by Section; the three finite width values likewise read the shared --eldra-container-{narrow,content,wide} tokens directly, through Container's own eldra-container-{narrow,content,wide} utilities rather than Tailwind's max-w-narrow/-content/-wide — see the Deviations entry below
    ContentCard--eldra-content-card-title-size (default 1.25rem) and --eldra-content-card-title-line (default 1.3) — the title's own size and line ratio, no token of its own; --eldra-content-card-excerpt-size (default 0.9375rem) and --eldra-content-card-excerpt-line (default 1.5) — the excerpt's own size and line ratio, shared by FeatureCard's body (text-content-card-excerpt in tailwind.css), since both read the same spec number
    CurrencyInputUnitInput's exactly (it renders a UnitInput with isCurrency always on) — see the UnitInput row
    Dialog--eldra-dialog-title-size (default 1.25rem), --eldra-dialog-title-line (default 1.3) — the title's own size/line, no token of its own; --eldra-dialog-width (default 32rem), --eldra-dialog-width-sm (default 24rem) — the panel's own width, always capped at the dialog's padded width (the viewport less 2rem) inside the utility itself; --eldra-dialog-max-height (default none: a tall panel grows and the dialog scrolls like a page; set it to cap the panel and scroll the body inside it instead); the eldra-dialog-fullscreen utility pins the panel to the viewport edges with the body scrolling between header and footer, for a consumer to put under a viewport variant (classes.panel: 'max-md:eldra-dialog-fullscreen'); --eldra-dialog-background (default var(--color-background)), --eldra-dialog-border (default var(--color-border)), --eldra-dialog-text (default var(--color-text), also the close button's hover and press tint), --eldra-dialog-muted (default var(--color-muted), the description) and --eldra-dialog-backdrop (default var(--color-overlay)) — the chrome's own colours, read by the dialog's parts only, so setting them on the <dialog> (class) recolours it without re-theming the slotted content the way redefining --eldra-color-* there would
    Drawer--eldra-drawer-title-size (default 1.25rem), --eldra-drawer-title-line (default 1.3) — the title's own size/line, no token of its own (the same numbers as Dialog's, but its own variable pair); --eldra-drawer-width (default 28rem) — the panel's own maximum width, capped at the viewport inside the utility itself, which also carries the full-screen-below-a-48rem-viewport media query; --eldra-drawer-header-height (default 4rem) — the header row's least height, with --eldra-drawer-header-padding (default var(--eldra-space-4)) — its top and bottom padding, 0px to centre the row's content in exactly that height, such as a site header's; --eldra-drawer-gutter (default 1.5rem) — the header's, body's and footer's side padding. The device's safe areas (env(safe-area-inset-*)) are added around both, so a full-screen drawer clears a notch and the home indicator on a page with viewport-fit=cover; --eldra-drawer-slide (default 2rem), --eldra-drawer-enter-duration (default 400ms), --eldra-drawer-exit-duration (default 260ms) — the panel's slide and fade in and out; --eldra-drawer-swipe-threshold (default 24px) — how far a side swipe travels before the panel follows; with mobileSheet, --eldra-drawer-sheet-gap (default 3rem), --eldra-drawer-sheet-radius (default var(--eldra-radius-xl)) and --eldra-drawer-handle-color (default var(--eldra-color-border-strong))
    EditorPlaceholder--eldra-editor-placeholder-border-width (default 1.5px) — the dashed boundary's width, distinct from EmptyState's own stock border (1px)
    EmptyState--eldra-empty-state-title-size (default 1.25rem) and --eldra-empty-state-title-line (default 1.3) — the title's own size and line ratio, no token of its own
    FeatureCardShares ContentCard's --eldra-content-card-excerpt-size/-line for its own body text (text-content-card-excerpt); its title reads text-h4 directly, no variable of its own
    FieldWrapper--eldra-field-note-line-height (default 1.45) — the line the help and error text share
    FilterPanel--eldra-filter-subheading-tracking (default 0.06em, a size-system sub-heading's letter spacing — the overline token's own 0.12em pushes "SOCKS (EU)" onto two lines in a 15rem sidebar), --eldra-filter-swatch-edge-width (default 1px) and --eldra-filter-swatch-ring-width (default 1.5px) for a colour swatch's edge and the ring it gains when picked, and --eldra-filter-histogram-bar-min / --eldra-filter-histogram-bar-radius (defaults 2px / 1px) for one histogram bar's minimum height and its two top corners. Everything else is a shared token or comes from the components it composes (Checkbox, Switch, Chip, Badge, Input, Button, CurrencyInput, RangeSlider), each restyled through its own row
    FormLayout--eldra-form-summary-radius (default var(--eldra-radius-md)) — the status summary box's corner radius
    Iconnone — reads only the shared tokens from layer 1
    Image--eldra-image-hatch-gap (default 0.75rem) — the live "No image" placeholder's diagonal-line repeat distance
    Input--eldra-input-radius (default var(--eldra-radius-md)), --eldra-control-font-size (default 0.9375rem), --eldra-control-font-size-mobile (default 1rem), --eldra-control-line-height (default 1.5rem), --eldra-field-border-width (default 1px)
    Lightboxnone of its own — the entrance keyframe (eldra-lightbox-in) and every size are either shared tokens or literal component-specific classes (px-16, size-11/max-md:size-11), the same "no dedicated variable" shape as AvatarGroup/ButtonGroup

版本与发布记录

@eldrajs/ui 共发布 21 个版本,首次发布于 2026/10/17;过去 12 个月发布了 21 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.6latest2026/11/121 天前
0.3.52026/11/121 天前
0.3.42026/11/121 天前
0.3.32026/11/121 天前
0.3.22026/11/121 天前
0.3.12026/11/112 天前

其他发布通道:placeholder → 0.0.0。

关于 @eldrajs/ui 的常见问题

@eldrajs/ui 是什么?

Accessible Vue 3 core components for Eldra storefronts: buttons, forms, selects, cards, overlays and navigation, themed through CSS design tokens.

如何安装 @eldrajs/ui?

在项目目录运行 npm install @eldrajs/ui,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:vue(^3.5.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@eldrajs/ui 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@eldrajs/ui 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@eldrajs/ui 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@eldrajs/ui 可以商用吗?

@eldrajs/ui 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@eldrajs/ui 的下载量有多少?

根据 npm 官方下载统计,@eldrajs/ui 在 2026/10/14 – 2026/11/12 这 30 天内被下载 3,656 次,日均约 122 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@eldrajs/ui 还在维护吗?

最新版本 0.3.6 发布于 2026/11/12(1 天前);至今共发布 21 个版本,其中过去 12 个月发布了 21 个;首次发布于 2026/10/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。