Component contract
이 콘텐츠는 아직 번역되지 않았습니다.
Component contract
Section titled “Component contract”Every platform exposes the same component names, variants, sizes, and tones.
| Component | Options | Rule |
|---|---|---|
| Button | variant: primary, secondary, ghost, danger. size: sm, md, lg. disabled, loading | Use at most one primary button per view, with a verb for the label. Secondary buttons are outlined. Renders as a rounded rectangle for pointer input and a capsule for touch |
| TextField | label, description, error, disabled | Always display a visible label; placeholder text is not a substitute for a label |
| Text | variant: the type roles. tone: primary, secondary, accent, danger, success, warning | Choose the semantic element for the document outline and set its visual style with variant |
| Badge | tone: neutral, accent, success, warning, danger. dot | Pill-shaped. The text label carries the meaning, while color reinforces it |
| Banner | tone: info, success, warning, danger. title | Each tone uses a distinct mark shape; warnings and errors announce immediately to screen readers |
| Card | title, description, actions, flush | Separated from the page by a hairline border and subtle shadow. flush removes internal padding when the card wraps a list |
| Divider, Spinner | A spinner is announced to assistive technologies only when given a label | |
| Switch | checked, label, disabled | Used for settings that take effect immediately. Remains clearly visible when off, with a track that holds 3:1 contrast against the page |
| Checkbox | checked, label, disabled | Used for selections that are applied later when submitting a form |
| SegmentedControl | options, value, full width | The active segment is raised: a hairline border, a faint shadow, and a darker label set it apart from the track |
| Avatar | name. size: sm, md, lg | Displays initials visually while exposing the full name to assistive technologies |
| ProgressBar | value from 0 to 1, label | Used for operations with known progress; use a Spinner for indeterminate work |
| Icon | name, size: sm, md, lg, label | Inherits the surrounding text color. Treated as decorative unless given a label. Arrows and chevrons can mirror in right-to-left layouts |
| ListRow | title, description, leading, trailing, chevron, disabled | Interactive when given an action. Separate adjacent rows with a Divider |
Three additional navigation components exist only on the touch-oriented platforms (React Native and Compose):
| Component | Options | Rule |
|---|---|---|
| TopBar | title, back action, trailing | Matches the page background with no bottom border. The back button is a circular control whose press area is as large as the minimum touch target |
| TabBar | items (label and icon), value | Holds three to five top-level destinations. Floats above the bottom edge as a capsule, with the active tab enclosed in its own pill so selection does not depend on color |
| BottomSheet | open, title, content | Floats inset from the screen edges. Closes when tapping the scrim, dragging down, or pressing the back button |
Icons are passed as slots: a TabBar item or a ListRow accepts any element, and Icon is the component the design system provides.
Interactive states:
| State | Rule |
|---|---|
| hover, pressed | Each state steps one shade darker; touch devices do not use hover. Pressing a button also scales it down slightly, providing non-color feedback |
| focus | Focus rings appear only during keyboard navigation |
| disabled | Recolored using dedicated disabled tokens rather than reduced opacity, since a semi-transparent colored fill can still look interactive |
| loading | Preserves both its text label (keeping button width stable) and its active colors, since the action is in progress rather than unavailable |
| invalid | Switches to a thicker red border and replaces the helper description with the error message; clear the error state as soon as the user edits the field |
Clearing the error state on edit avoids a common web form trap: when an invalid state blocks form submission, leaving the field marked invalid while the user types prevents them from submitting the corrected value.
Platform implementations
Section titled “Platform implementations”| Web | Desktop | Mobile | Kotlin | |
|---|---|---|---|---|
| Package | @acme/ui |
acme-ui |
@acme/ui-native |
kotlin/ui |
| Built on | HeroUI v3 (React Aria) and native inputs | GPUI Component | React Native primitives | Compose foundation |
| Runs on | Browsers | macOS | iOS, Android | Android, iOS, desktop (JVM) |
| Tokens arrive as | CSS custom properties and a Tailwind theme | Rust constants | TypeScript constants | Compose values (Color, Dp, TextStyle) |
| Sizes | Follow the input device | Pointer | Touch | Touch on phones, pointer on desktop |
| Design preview | /design |
acme --design |
/design |
The button on the home screen |
On the web, heroui-theme.css maps HeroUI’s theme variables to the generated tokens, and components.css applies the token sizes and state colors to HeroUI’s button and input primitives. Those two stylesheets are the only files coupled to HeroUI’s internal naming. Button and TextField wrap HeroUI components for their interaction and accessibility behavior. Switch, Checkbox, and SegmentedControl use native HTML inputs (a checkbox or radio group) with custom visuals drawn alongside a hidden input, so keyboard navigation, forms, and screen readers interact with standard controls. The remaining components are plain HTML elements styled with Tailwind utilities. Any unwrapped HeroUI component still inherits the token colors and border radii through the theme variables.
On desktop, acme_ui::set_mode applies the generated tokens to GPUI Component’s theme. button, switch, and checkbox return GPUI Component controls configured with token values, while Badge, Banner, TextField, SegmentedControl, Avatar, ProgressBar, ListRow, card, divider, and text are rendered directly with GPUI primitives.
On mobile, components are built directly on React Native’s Pressable and TextInput without a third-party UI library, and text scales with the OS font size preference.
The Kotlin client is a single Compose Multiplatform library targeting Android, iOS, and desktop. Like the React Native implementation, it builds on Compose foundation without Material components so that all styling comes directly from the tokens.
Platform differences:
| Difference | |
|---|---|
| Disabled controls | Web, mobile, and Compose recolor disabled controls with dedicated tokens; on the GPUI desktop client, GPUI Component dims buttons, switches, and checkboxes with opacity |
| An invalid field | Web and mobile thicken the field border; desktop draws a second outline around the field |
| The banner’s mark | Web renders a shape icon; desktop and mobile render a letter inside a circle |
The design preview
Section titled “The design preview”Every platform includes a design preview screen that renders all tokens and components in the same order: colors, typography, spacing/radius/sizes, buttons, text fields, selection controls, badges, banners, cards, lists, and (on touch platforms) navigation components. This makes it easy to place two platforms side by side for visual comparison. Color swatches are generated directly from the token exports, so newly added color tokens appear in the preview automatically.
| How to open it | Entry point | Light and dark | |
|---|---|---|---|
| Web | mise run //web:dev, then http://localhost:5173/design |
The button on the home screen | Follows the system. ?theme=light and ?theme=dark pin the mode |
| Desktop | mise run //desktop:design or acme --design |
The button in the home window | Follows the system. Use the window toggle button, or pass --light or --dark |
| Mobile | The /design screen |
The button on the home screen | Follows the system |
| Kotlin | mise run //kotlin:design on desktop; the home screen button elsewhere |
The button on the home screen | Follows the system. Includes an in-app mode switch |
To support automated screenshot scripts that cannot scroll or tap interactively: web sections expose URL hash anchors (/design#buttons), the desktop binary accepts --scroll <pixels>, and the mobile preview accepts ?from=<section number> and ?sheet=1.
On the web, the preview code is lazy-loaded only when navigating to /design. Static file hosts must be configured to serve index.html for /design.