@api7/ui-components

中文文档
0.7.0发布于 2026/11/02TypeScriptES Module近 30 天下载 7739

API7 React component library

原始使用文档
文档目录14 节
  1. Installation
  2. Quick start
  3. Entry points
  4. Buttons
  5. Spinners
  6. Progress bars
  7. Forms
  8. With another form library
  9. Joining a field and a button
  10. Description lists
  11. Theming
  12. Dark mode
  13. Using it with your Tailwind CSS
  14. Popover arrows

@api7/ui-components

API7 React component library built on Base UI and Tailwind CSS v4. The components are styled with Tailwind utility classes, and the application's own Tailwind CSS compiles them, so the library and the application share one set of utilities and one theme.

Installation

The library needs React 19 and an application that already builds with Tailwind CSS 4.1 or newer. If the application does not use Tailwind yet, follow the Tailwind CSS installation guide for your build tool first, then come back here.

npm install @api7/ui-components tailwindcss react-day-picker lucide-react
# or
pnpm add @api7/ui-components tailwindcss react-day-picker lucide-react
# or
yarn add @api7/ui-components tailwindcss react-day-picker lucide-react

tailwindcss is already there if the application uses Tailwind. Install the integration of Tailwind for your build tool as well (@tailwindcss/vite, @tailwindcss/postcss or @tailwindcss/cli). The import of the theme works with the Vite plugin, the PostCSS plugin and the CLI, which are the ones tested. Other build tools, such as webpack, are not tested. The package expects these peer dependencies in the application:

PackageVersionNeeded for
react, react-dom^19Everything
tailwindcss^4.1.0Compiles the classes of the components and registers the theme. See Quick start
lucide-react^1Icons used inside components
react-day-picker^10.0.1Required. Calendar, DatePicker and DateRangePicker import it, and the package declares it as a required peer, so install it even if you do not use them
@tanstack/react-form^1.33.0@api7/ui-components/form (optional)
@tanstack/react-table^9.0.0@api7/ui-components/data-table (optional)

Add the optional ones only if you import those entry points.

Quick start

  1. In the main CSS file of the application (for example src/index.css), import Tailwind CSS and then the library theme, and make sure the entry file of the application imports that CSS file:

    @import 'tailwindcss';
    @import '@api7/ui-components';
    

    The order matters. The theme extends the Tailwind theme, so it has to come after tailwindcss.

    In CSS the package name resolves to the theme through the style export condition, and in JavaScript it resolves to the components. Tailwind CSS 4 and the tools that resolve CSS imports the way it does (the Tailwind CLI, @tailwindcss/postcss and @tailwindcss/vite) apply the condition. A tool that ignores it finds the JavaScript entry instead and fails on @import.

    That is all the configuration there is. The theme file points Tailwind at the classes the components use, so you do not add an @source line for the library, and it registers the semantic colors (bg-primary, text-muted-foreground, border-border and the rest), the radii and the font as Tailwind theme values. The application keeps its own Tailwind palette, and the same classes work in your own markup. See Using it with your Tailwind CSS for what the theme changes in the application.

  2. Load a font for --font-sans. It is Inter by default. For example, install @fontsource-variable/inter (npm install @fontsource-variable/inter) and import it in the same CSS file, or in the entry file of the application:

    import '@fontsource-variable/inter';
    

    To use another font, set --font-sans in an @theme block after the imports:

    @theme {
      --font-sans: 'Your Font', sans-serif;
    }
    

    The library does not style the page. Components inherit the text color, the font and the font size from the page, so set them on body in the base layer of the application. The components are laid out and tested with this rule, and a different line height changes the height of components that do not set their own:

    @layer base {
      body {
        background-color: var(--color-background);
        color: var(--color-foreground);
        font-family: var(--font-sans);
        font-size: var(--text-sm);
        line-height: var(--text-sm--line-height);
      }
    }
    
  3. Put isolate on the root element of the application (<div id="root" class="isolate">). Popups, dialogs and drawers are rendered into <body>, outside your root, and they have z-index: 50 (the toast has 1010). An element of the application with a higher z-index, or a root with a z-index of its own, is painted over a popup that overlaps it. isolate makes the z-index values of the application local to the root, so every popup stays above it whatever values the application uses.

  4. Use the components. No provider is required. Render Toaster once if you use toast, and wrap the application in ConfigProvider if you need a locale other than en-US:

    import { Button, ConfigProvider, Toaster, toast } from '@api7/ui-components';
    
    export function App() {
      return (
        <ConfigProvider locale="zh-CN">
          <Button onClick={() => toast.success('Saved', { description: 'Your changes were stored.' })}>Save</Button>
          <Toaster />
        </ConfigProvider>
      );
    }
    

ConfigProvider accepts locale (en-US or zh-CN) and messages, an object that overrides the built-in texts, such as the loading label of a button or a spinner, the close buttons, the empty state of the data table and the pagination labels. Define messages at module scope or memoize it.

Entry points

ImportContents
@api7/ui-componentsComponents, toast and Toaster, ConfigProvider, useLocale and useMessages, and in CSS the theme for the Tailwind CSS of the application
@api7/ui-components/formForm fields built on TanStack Form. Their form row, LabeledField, is in the main entry and works with any form library
@api7/ui-components/data-tableThe data table built on TanStack Table
@api7/ui-components/tokensDesign token data (palette, themes, FAMILIES, STEPS and the contrast helpers) for documentation pages

The package is ESM only. Component modules keep their "use client" directive, so the components work in a React Server Components setup such as the Next.js app router.

Buttons

Button takes a variant: primary (the default), secondary, outline, ghost, destructive or link. It takes a size: sm, md (the default), lg or icon. loading disables the button and shows a spinner, and loadingLabel sets the text that screen readers announce.

import { Button } from '@api7/ui-components';

export function Actions() {
  return (
    <>
      <Button>Save</Button>
      <Button variant="outline" size="sm">
        Cancel
      </Button>
      <Button variant="destructive" loading loadingLabel="Deleting">
        Delete
      </Button>
    </>
  );
}

A link that looks like a button is an ordinary anchor or router link styled with buttonVariants. It carries the same classes as a Button:

import { buttonVariants } from '@api7/ui-components';

export function DocsLink() {
  return (
    <a href="/docs" className={buttonVariants({ variant: 'outline' })}>
      Docs
    </a>
  );
}

Spinners

Spinner is an SVG that spins, and it is a status, so a screen reader announces it. It takes a size: xs, sm, md (the default) or lg. Its colour is the text colour, so set it with a text-* class, and any other className is merged onto the SVG.

import { Spinner } from '@api7/ui-components';

<div className="flex justify-center py-12">
  <Spinner />
</div>;

Things worth knowing before you build on it:

  • The announced name is the spinner.label message of the ConfigProvider, and an aria-label on the spinner replaces it, for example to say what is loading.
  • The spinner mounts together with the loading state, and whether a screen reader announces a status that is inserted into the page is up to the screen reader. To make the loading state reliably announced, set aria-busy or aria-live="polite" on a container that is already on the page and put the spinner inside it.
  • It does not wrap other elements. To spin your own icon, add animate-spin to it.
  • It stops spinning when the user prefers reduced motion, and then shows the static icon.
  • It does not position itself. A loading overlay over a table or a card is built where you use it, with the layout that place needs. Button has its own loading prop, use that for a button.

Progress bars

Progress shows how far a task has come. It takes a value between min (0) and max (100), and a null value makes the bar indeterminate, for a task whose length is unknown. It takes a size: xs, sm (the default) or md, which are 4, 6 and 8px high. It takes a variant: default, success, warning or destructive, the colour of the fill. The bar fills the width of its container, so set the width on the container or with className.

import { Progress } from '@api7/ui-components';

<div className="flex flex-col gap-1">
  <div className="flex justify-between text-xs">
    <span id="cores-label">Cores</span>
    <span>45 / 100</span>
  </div>
  <Progress value={45} aria-labelledby="cores-label" />
</div>;

Things worth knowing before you build on it:

  • There is no default accessible name, because only you know what is progressing. Give the bar an aria-label, or an aria-labelledby that points at the text next to it, as above.
  • The label and the numbers around a bar are laid out where you use it. There is no label or value part.
  • The variant is a fixed colour. A bar that changes colour as it fills, such as a quota that turns red near its limit, is a variant that you compute from the value where you use it.
  • An indeterminate bar slides a segment along the track. When the user prefers reduced motion it shows the whole track at half opacity instead of a static segment, so it is not read as a partial value. The width of a determinate bar is animated too, and that stops as well.
  • The fill uses the text colour of each tone (primary-text, success-foreground, warning-foreground and destructive-text), because those stay 3:1 against the track in both themes, and the plain tone colours do not.

Forms

@api7/ui-components/form wraps TanStack Form. useAppForm returns a form whose AppField hands you ready made fields: TextField, TextareaField, NumberField, SelectField, ComboboxField, DatePickerField, DateRangePickerField, CheckboxField, SwitchField and RadioGroupField:

import { Button } from '@api7/ui-components';
import { useAppForm } from '@api7/ui-components/form';

export function NameForm({ onSave }: { onSave: (name: string) => void }) {
  const form = useAppForm({
    defaultValues: { name: '' },
    onSubmit: ({ value }) => onSave(value.name),
  });

  return (
    <form
      noValidate
      onSubmit={(event) => {
        event.preventDefault();
        void form.handleSubmit();
      }}
    >
      <form.AppField name="name" validators={{ onChange: ({ value }) => (value ? undefined : 'Name is required') }}>
        {(field) => <field.TextField label="Name" />}
      </form.AppField>
      <Button type="submit">Save</Button>
    </form>
  );
}

Validators are the ones of TanStack Form, so a function or a Standard Schema such as a zod schema both work. A string returned by a validator is shown under the field.

With another form library

The form row that these fields draw is LabeledField from the main entry, and it does not depend on TanStack Form. It takes the label, labelTooltip, optional, description and layout like the fields above, and the state of the field as props: name, invalid, touched, dirty and errors, a list of messages shown while invalid is true. The control inside it gets its id, aria-invalid and aria-describedby from the field, so it is wired the same way as in a TanStack form. group renders a fieldset with the label as its legend, for a group of controls such as radio buttons.

With react-hook-form, read the state with useController and pass it in:

import { Input, LabeledField } from '@api7/ui-components';
import { useController } from 'react-hook-form';

export function NameField() {
  const { field, fieldState } = useController({ name: 'name', rules: { required: 'Name is required' } });
  return (
    <LabeledField
      label="Name"
      name={field.name}
      invalid={fieldState.invalid}
      touched={fieldState.isTouched}
      dirty={fieldState.isDirty}
      errors={fieldState.error?.message ? [fieldState.error.message] : []}
    >
      <Input ref={field.ref} value={field.value} onValueChange={field.onChange} onBlur={field.onBlur} />
    </LabeledField>
  );
}

invalid decides when the error shows. The fields of /form show it once the field is touched, and react-hook-form sets fieldState.invalid after its own validation mode runs, on submit by default.

Joining a field and a button

InputGroup joins an Input with a Button or an InputGroupAddon into one control. The group draws no border, ring or state of its own. Each part keeps its own border, hover, focus, invalid and disabled styling, and they touch with one divider between them, so the field and the button read as one. Only the outer corners of the group are round. An Input and a Button that stand alone are not changed.

import { Button, Input, InputGroup, InputGroupAddon } from '@api7/ui-components';

export function TokenField({ token, onCopy }: { token: string; onCopy: () => void }) {
  return (
    <InputGroup>
      <Input aria-label="Token" readOnly value={token} />
      <Button variant="outline" onClick={onCopy}>
        Copy
      </Button>
    </InputGroup>
  );
}

export function HostField() {
  return (
    <InputGroup>
      <InputGroupAddon>https://</InputGroupAddon>
      <Input aria-label="Host" />
      <InputGroupAddon>.example.com</InputGroupAddon>
    </InputGroup>
  );
}

The parts are the direct children of the group, in any order. An InputGroupAddon is a block with the muted background of the legacy console for text, an icon or a button, and a Button inside it loses its own surface. The state belongs to the part that has it: a focused field is framed alone, a button keeps the focus it has when it stands alone (a ring for the keyboard, none for a click), and an invalid field (aria-invalid, or a Field that is invalid) has the invalid border on the field only, as a disabled field has the disabled background on the field only. An outline button has the border colour of an input. A button with a fill (primary, secondary or destructive) keeps its own edge: at rest the border of the field toward it is transparent, so no line crosses the fill. The group is 32px high like an Input, and the parts stretch to that height, so they take no height of their own. Put the group inside a Field and a FieldLabel labels the field in it as it does any Input.

Description lists

DescriptionList shows read-only label and value pairs, such as the fields of a detail page. It is composed from DescriptionListItem, DescriptionListLabel and DescriptionListValue, and there is no data array prop. To render a list of records, map over them where you use it, and filter out the ones you do not want to show there too.

import {
  DescriptionList,
  DescriptionListItem,
  DescriptionListLabel,
  DescriptionListValue,
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from '@api7/ui-components';
import { Info } from 'lucide-react';

export function GatewayDetails({ gateway }: { gateway: { id: string; name: string; verifyTls: boolean } }) {
  return (
    <DescriptionList variant="outline" columns={2}>
      <DescriptionListItem colSpan={2}>
        <DescriptionListLabel>ID</DescriptionListLabel>
        <DescriptionListValue>{gateway.id}</DescriptionListValue>
      </DescriptionListItem>
      <DescriptionListItem>
        <DescriptionListLabel>Name</DescriptionListLabel>
        <DescriptionListValue>{gateway.name}</DescriptionListValue>
      </DescriptionListItem>
      <DescriptionListItem>
        <DescriptionListLabel>
          Verify TLS
          <Tooltip>
            <TooltipTrigger aria-label="About Verify TLS">
              <Info />
            </TooltipTrigger>
            <TooltipContent>Verify the certificate of the upstream server.</TooltipContent>
          </Tooltip>
        </DescriptionListLabel>
        <DescriptionListValue>{gateway.verifyTls ? 'Enabled' : 'Disabled'}</DescriptionListValue>
      </DescriptionListItem>
    </DescriptionList>
  );
}

variant is default, a list without borders, or outline, which reproduces the bordered grid of the legacy console detail pages with a tinted label column. columns is how many label and value pairs sit on one row and defaults to 1. An item takes a colSpan to span several of them. A fractional columns or colSpan is rounded down, one that is below 1 or not a number counts as 1, and a colSpan larger than columns is reduced to columns.

Things worth knowing before you build on it:

  • The markup is a dl, and every item is a div that holds one dt and one dd. The label and the value are adjacent siblings, so a test can take the value of a label with its next sibling. There is no row element, because the browser lays the items out with CSS grid, so select the items, not rows.
  • A row that is not filled, for example the third item with columns={2}, leaves the rest of the row empty. The last item does not stretch.
  • The label column is 160px wide, as in the legacy console. Set --description-list-label-width in style or className on the list to change it.
  • A value can hold any content. It grows with its content and long text wraps, so do not give a value a fixed height.
  • A tooltip in a label is a Tooltip you place there, and its trigger needs an accessible name, as it does anywhere else.

Theming

The library theme is the default theme of the application. The colors, the radii and the font are Tailwind theme variables, such as --color-primary and --font-sans, and the application overrides them in its own CSS after the import, as Using it with your Tailwind CSS explains. The light values are the values of the page, [data-theme=light] repeats them for a region, and [data-theme=dark] holds the dark values. The values themselves are in src/tokens/semantic.ts.

Dark mode

Set data-theme="dark" on <html> and every component renders dark, including popups, dialogs, drawers and toasts, because they are rendered into <body> and inherit from <html>. Without the attribute, or with any value other than dark, the light theme applies. Removing the attribute or changing its value restyles the page at once, there is no library state to keep in step.

The library only reads the attribute. It never follows prefers-color-scheme, never stores anything and never writes to document, so an application without a dark design does not turn dark because a visitor's browser prefers it. Choosing the theme, remembering it and following the system, if the application wants that, are the application's job. With next-themes it is <ThemeProvider attribute="data-theme" defaultTheme="light" enableSystem={false}>, and the same works with any code that sets the attribute.

color-scheme follows the theme as well, so scrollbars and native form controls match.

The library defines the Tailwind dark variant as "inside a data-theme=dark element", so dark:bg-muted in your own markup follows the same attribute. If the application declares its own @custom-variant dark after the import, for a .dark class or the media query, that declaration replaces the library one, and the dark: classes of the components follow it as well. The color variables still switch only with data-theme, so such an application gets the light tokens together with the dark: adjustments of the components, a mixed look. Set data-theme as well, or do not redefine the variant.

A region with another theme

data-theme="light" or data-theme="dark" on any element sets the color variables and the color-scheme of everything inside it, and the nearest ancestor wins, so a dark page can hold a light region and the other way round. This works for the color variables and for the dark: styles alike. The one case plain CSS cannot express is a dark element inside a light element inside a dark element: the innermost dark element is read as light in the dark: styles, though its variables are right.

The attribute does not paint a background or a text color, so a region that should look like a page of its own needs bg-background text-foreground (or your own rules) on it. A popup is rendered into <body>, so it follows <html>, not the region that opened it. To theme a popup, put the attribute on the popup element itself (<PopoverContent data-theme="light">, which works for the content of Popover, Menu, Select, Combobox, Tooltip, Dialog and Drawer, because each passes its props to the popup element), or on an element inside the popup's content, which then applies to what it wraps.

Writing dark styles in a component

A component styles itself with semantic tokens, and each token has a light and a dark value, so most components need nothing for dark mode. When a dark theme needs a different treatment than a token swap, a component may add a dark: class together with a semantic token (dark:bg-input/30). Raw palette steps (bg-blue-100) are not used in a component at all, because the application's palette is the application's own. Badge is the one component with a color per palette family, and it reads the private variables --badge-<color>-bg and --badge-<color>-fg, which the theme sets for each theme from BADGE_STEPS in src/tokens/contrast.ts. A brand color has two tokens because a fill and a text color need different lightness in dark: primary and destructive are fills (with primary-foreground on top), while primary-text and destructive-text are for text and icons on a surface, such as the Button link variant, a selected Tab, error text or the destructive menu item. They equal the fill in light, so a page that colors text with text-primary passes in light and fails contrast in dark. Prefer a token to dark: wherever the token can carry the difference. src/theme-classes.test.ts enforces these rules. The values of the dark tokens are in src/tokens/semantic.ts, and every text and surface pair of both themes is tested for WCAG AA in src/tokens/contrast.test.ts.

Using it with your Tailwind CSS

The components are ordinary Tailwind utilities, so there is one stylesheet and one cascade. The classes of the components are generated into the utilities layer of the application, next to its own utilities, and a className you pass to a component replaces the default classes that set the same property. Passing p-6 to a component that has px-3 leaves only p-6.

What the theme changes in the application:

  • It adds the semantic colors to the Tailwind theme: background, foreground, card, popover, primary, primary-text, muted, accent, destructive, success, warning, info, border, input, ring and the others listed in src/tokens/semantic.ts. The names are common, so an application that already defines --color-primary or --color-muted gets the value of whichever declaration comes last.
  • It sets rounded-sm to 2px, rounded-md to 4px and rounded-lg to 8px, for the application's own markup as well. The 8px size is for outer containers, while the existing control sizes stay unchanged. Other radii and the whole color palette stay as Tailwind defines them.
  • It sets --font-sans. Tailwind's theme points --default-font-family at it, so Inter becomes the default font of the whole application, not only of the font-sans utility.
  • It defines the dark variant, as described in Dark mode.

To change a color, redeclare the variable after the import, on the same selectors the library uses, so that both themes and the nested regions keep working:

@import 'tailwindcss';
@import '@api7/ui-components';

:root,
[data-theme='light'] {
  --color-primary: #0b5cff;
}

[data-theme='dark'] {
  --color-primary: #6ea0ff;
}

An @theme block with the same variable changes only the value of a page root that has no data-theme attribute. A data-theme region, including <html data-theme="…">, keeps the library value, because the library rules are not in a layer and Tailwind emits @theme values in the theme layer. Use @theme only for values that do not change with the theme, such as --font-sans.

Popover arrows

PopoverContent has no arrow by default. Add PopoverArrow inside it for a pointer that follows the trigger when viewport collision handling shifts or flips the popup. The arrow uses the popover background token and accepts the Base UI arrow props, including className and style for appearance overrides.

import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from '@api7/ui-components';

<Popover>
  <PopoverTrigger>View ID</PopoverTrigger>
  <PopoverContent align="center" sideOffset={12}>
    <PopoverArrow />
    ID: service-production
  </PopoverContent>
</Popover>;

版本与发布记录

@api7/ui-components 共发布 11 个版本,首次发布于 2026/10/19;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.7.0latest2026/11/025 天前
0.6.02026/11/025 天前
0.5.02026/10/2810 天前
0.4.02026/10/2711 天前
0.3.02026/10/2612 天前
0.2.12026/10/2514 天前

关于 @api7/ui-components 的常见问题

@api7/ui-components 是什么?

API7 React component library

如何安装 @api7/ui-components?

在项目目录运行 npm install @api7/ui-components,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19)、react-dom(^19)、tailwindcss(^4.1.0)、lucide-react(^1)、react-day-picker(^10.0.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@api7/ui-components 支持 TypeScript 吗?

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

@api7/ui-components 支持 ESM 还是 CommonJS?

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

@api7/ui-components 需要什么版本的 Node.js?

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

@api7/ui-components 可以商用吗?

@api7/ui-components 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@api7/ui-components 的下载量有多少?

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

@api7/ui-components 还在维护吗?

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