Beta 1 [v3] (#5872)
* refactor: compound and named pattern (#5850) * refactor(component): compound and named pattern * refactor(component): tabs component naming consistency * docs(changelog): v3.0.0-alpha-36 * fix(docs): correct broken storybook links (#5854) * feat(components): checkbox (#5829) * fix(component): support compound pattern in server component * fix(component): keep accordion and chip in sync with compound pattern * fix(component): type of forward refs * fix(calendar): temporary workaround for ref error * fix(storybook): imports * feat(checkbox): initial draft * refactor(styles): remove orientation * refactor: migration from dot notation to separted components to support RSC * refactor: migrate to React 19 ref pattern - remove forwardRef wrappers and explicit ref declarations from all components * chore(checkbox): revise checkbox story * feat(checkbox): checkmark & indeterminate * chore(styles): checkmark & indeterminate styles * refactor: provider context removed as it is not longer needed on react 19 * feat: made the migration smoother by still supporting the "dot" exports but adjusting the main compound component * fix: compound patter, radio group api, ref on react 19 * refactor(switch): split switch and switch-group into separate components following radio/radio-group pattern * chore(changelog): update v3.0.0-alpha.35 release notes and date * fix(page): update version label to reflect RSC support * chore(changelog): update examples to use new component names CardRoot and TabsRoot * refactor(checkbox): adopt latest component structure * chore: remove radio stories * refactor(checkbox): adopt new api and revised stories * refactor(checkbox-group): adopt new api and add stories * chore(checkbox-group): revise examples * chore(checkbox): separate checkbox group css * chore(styles): revise checkbox styels * fix(styles): add border for invalid * feat(checkbox): add invalid story * chore: sync changes from v3 redesign * refactor(checkbox): remove slots * refactor(checkbox): apply compound and named pattern * feat(docs): checkbox page * refactor(checkbox): remove use client * chore(docs): revise based on the latest revamp --------- Co-authored-by: Tianen Pang <32772271+tianenpang@users.noreply.github.com> Co-authored-by: Junior Garcia <jrgarciadev@gmail.com> * V3 Redesign (#5851) * fix(docs): update Link component to use Link.Root for consistency with new component structure * feat: light theme in progress, button, spearator & accordion adjusted to new default theme * feat: button and chip done * feat: enhance Skeleton component with new Grid and Single Shimmer previews; update card and disclosure styles for consistency * feat: update Kbd component styles and add Abbr support in stories * feat: avatar and chip updated * fix(accordion): adjust separator styling for consistency in accordion items * refactor: close button adjsuted * feat(link): add underline variants and offset options to Link component documentation and implementation * feat(link): enhance link component with hover state and transition effects for icon opacity * refactor: surfaces variables are now calculated by default, card, button, disclosures updated * refactor: update styles for various components to improve consistency and reduce motion effects * feat(storybook): add welcome story and update story order; feat(styles): include variants in package config and copy script * refactor: update accordion, card, and kbd components to enhance styling and introduce new variants * refactor(link): update hover underline effect to use opacity transition instead of scale transformation * refactor(styles): update background color variables and remove gradient backgrounds from layout components * refactor(storybook): enhance color stories by adding calculated color variants and removing deprecated soft colors * refactor(styles): introduce background quaternary color and update shadow variables for popover and tooltip components * feat: alert component created * refactor(alert-demo, x-profile-demo): simplify button usage and enhance layout alignment * feat: input otp * feat: inpuit otp docs * refactor(switch): enhance styling with improved transition timings and add box-shadow for active states * feat: home page design in progress * feat: add Surface component with context-based on-surface styling - Add Surface component with four variants (default, secondary, tertiary, quaternary) - Implement SurfaceContext to provide variant to child components - Update Input, TextArea, InputOTP, and RadioGroup to automatically detect Surface context - Components now apply on-surface styling when wrapped in Surface component - Add OnSurface stories for all affected components - Create comprehensive Surface documentation with examples - Add OnSurface demo examples to all component documentation pages - Update CSS classes to support on-surface modifier variants * feat: add Surface component documentation and update meta.json * feat(card): implement form handling in WithForm story using Form, Input, and Label components * feat(card): refactor WithForm to use Form component for improved form handling and submission * refactor(accordion): remove background class from stories and enhance AccordionRoot with SurfaceContext for elevated variant * refactor(accordion): update variant from elevated to surface across components and documentation * feat: add checkbox-group component and checkbox enhancements * feat(demo): add UIComponentsDemo to the DemoComponents * refactor: migrate Component.Root to Component in docs and demos - Update all component documentation files to use Component instead of Component.Root - Update all demo files to use simplified component syntax - Update API reference headers from Component.Root Props to Component Props - Exclude changelog files from migration as requested This change promotes the simpler and more elegant Component syntax while maintaining backward compatibility (both patterns are still supported). * style: update global styles and card component * feat(storybook): enhance color stories with new "On Surface" color variants and remove obsolete field color states * feat(demos): update chip component props from 'type' to 'color' and add "use client" directive in multiple demo files * feat(demos): add "use client" directive to basic button demo * refactor: theme variables update background color in dark mode * feat(demos): add "use client" directive to multiple demo files * feat(demos): add FieldError component to TextfieldDemo for improved validation feedback * fix(demo): update avatar images and names in avatar-group-demo for consistency * feat: listbox component preview created and documented * feat: selecte component created * feat: export React Aria components from rac module * feat: slider component created, documentation styles and content updated. Home page completed * fix(demo): correct spelling of "credits" in AlertDemo component * refactor(docs): simplify component usage by using compound pattern (#5870) * fix(component): checkbox build errors * refactor(docs): simplify component usage by using compound pattern * fix(component): checkbox build errors * chore(docs): correct pattern description * feat(docs): enhance component preview with intersection observer for lazy loading * feat(docs): enhance code block styling and functionality with isolated rendering option * chore(root): upgrade package manager * fix(docs): update image sources and enhance styling across various components --------- Co-authored-by: Tianen Pang <32772271+tianenpang@users.noreply.github.com> * fix(docs): update Twitter card image and adjust styles in route component for improved readability * refactor(docs): updated color documentation * fix(docs): update icon import in home page and remove unused client directive from Iconify component * chore(docs): update documentation to reflect beta status and adjust storybook links for components * feat: fix alert responsive * feat(docs): replace img tags with next/image component for improved performance and consistency in home page and demo components * fix: improve Lighthouse performance scores - Replace picture/source elements with Next.js Image component - Add fetchPriority='high' and loading='eager' to LCP images - Fix viewport: remove userScalable: false and increase maximumScale to 5 - Add robots metadata with index: true for proper SEO - Update sitemap URL in robots.txt * fix: explicitly set X-Robots-Tag header to ensure proper indexing - Add headers function in next.config.ts to set X-Robots-Tag: index, follow - This ensures the header is explicitly set and prevents noindex issues * fix(docs): update Link component styling in home page for improved visibility * feat: card demos adjusted * fix(docs): update introduction images for consistency and improved presentation * fix(docs): update image sources in introduction and README for consistency * fix(docs): update card component to include icon property for enhanced documentation * fix(docs): update icon property for ListBox, Select, Slider, and MCP Server components to 'new' for improved documentation clarity * fix(docs): update card demo layout to use flex-wrap for improved responsiveness * feat(docs): implement changelog filtering in search API response * feat: beta-1 changelog (draft) * fix(docs): update version label in home page to reflect major redesign * feat(docs): update changelog for v3-0-0-beta-1 with new components and improved API details * fix(docs): correct formatting in changelog for v3-0-0-beta-1 * chore(docs): minor changes on v3 beta doc (#5879) * chore(docs): update release date * chore(docs): point to v3 component doc instead of v2 * chore(docs): add updated icon * fix(input): typo * refactor(styles): reduce motion (#5873) * refactor(styles): reduce motion * refactor(styles): reduce motion * docs(handbook): disabling animations in animation section * style(component): extend motion-reduce and adjust motion priority * docs(handbook): disabling animations in animation section * chore(docs): update design principles and changelog for v3-0-0-beta-1 * feat(docs): enhance HomePage and DocsImage components with static rendering and improved image handling * fix(docs): update background colors for input, textarea, skeleton components and tabs in showcase --------- Co-authored-by: Tianen Pang <32772271+tianenpang@users.noreply.github.com> Co-authored-by: WK <wingkwong.code@gmail.com>
This commit is contained in:
@@ -52,7 +52,7 @@ You communicate technical concepts clearly and can assist both human developers
|
||||
|
||||
```css
|
||||
/* Good */
|
||||
@apply bg-panel rounded-lg border p-4 shadow-md;
|
||||
@apply bg-surface rounded-lg border p-4 shadow-md;
|
||||
```
|
||||
|
||||
2. **Keep custom CSS properties**: Don't convert properties without Tailwind utilities
|
||||
|
||||
@@ -343,7 +343,7 @@ Tailwind CSS v4 embraces CSS custom properties for theming and dynamic values:
|
||||
```css
|
||||
.component {
|
||||
/* Good: Use @apply for Tailwind utilities */
|
||||
@apply bg-panel rounded-lg border p-4 shadow-md;
|
||||
@apply bg-surface rounded-lg border p-4 shadow-md;
|
||||
|
||||
/* Keep custom CSS for non-utility properties */
|
||||
cursor: var(--cursor-interactive);
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"npm.packageManager": "pnpm",
|
||||
"css.lint.emptyRules": "ignore",
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"scss.lint.unknownAtRules": "ignore",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
|
||||
@@ -12,6 +12,24 @@ icon: clock-arrow-rotate-left
|
||||
## Latest Release
|
||||
|
||||
|
||||
### v3.0.0-beta.1
|
||||
|
||||
**November 6, 2025**
|
||||
|
||||
<VideoPlayer
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/videos/v3-beta-1.mp4"
|
||||
poster="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/videos/v3-beta-1-poster.png"
|
||||
height={430}
|
||||
/>
|
||||
|
||||
This release introduces a comprehensive redesign of HeroUI v3, merging v2's beauty and animations with v3's simplicity. All components have been redesigned, 8 new components added ([Alert](/docs/compoenents/alert), [Checkbox](/docs/components/checkbox), [InputOTP](/docs/components/input-otp), [ListBox](/docs/components/listbox), [Select](/docs/components/select), [Slider](/docs/components/slider), [Surface](/docs/components/surface)), and the design system has been completely overhauled with better color tokens, shadows, and architecture. Includes breaking changes to design system variables, component APIs, and flexible component patterns.
|
||||
|
||||
[Read full release notes →](/docs/changelog/v3-0-0-beta-1)
|
||||
|
||||
---
|
||||
|
||||
## Previous Releases
|
||||
|
||||
### v3.0.0-alpha.35
|
||||
|
||||
**October 21, 2025**
|
||||
@@ -37,10 +55,6 @@ All compound components now require `.Root` suffix: `Accordion`, `Avatar`, `Card
|
||||
|
||||
[Read full release notes →](/docs/changelog/v3-0-0-alpha-35)
|
||||
|
||||
---
|
||||
|
||||
## Previous Releases
|
||||
|
||||
### v3.0.0-alpha.34
|
||||
|
||||
**October 15, 2025**
|
||||
@@ -94,9 +108,9 @@ Card component redesigned with [new variants](/docs/components/card), added [Clo
|
||||
|
||||
HeroUI v3 follows a regular release cycle:
|
||||
|
||||
- **Alpha releases**: Weekly to bi-weekly during active development - In progress
|
||||
- **Beta releases**: Monthly stabilization releases (once first beta release is made)
|
||||
- **Stable releases**: Quarterly major versions (once first stable release is made)
|
||||
- **Alpha releases**: Weekly to bi-weekly during active development
|
||||
- **Beta releases**: Monthly stabilization releases - In progress
|
||||
- **Stable releases**: Quarterly major versions (Q4 2025 target)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"title": "Changelog",
|
||||
"description": "HeroUI v3 Release Notes",
|
||||
"root": false,
|
||||
"pages": ["changelog", "v3-0-0-alpha-35"]
|
||||
"pages": ["changelog", "v3-0-0-beta-1"]
|
||||
}
|
||||
|
||||
@@ -271,15 +271,15 @@ import {
|
||||
|
||||
**Tabs Example:**
|
||||
```tsx
|
||||
import { TabsRoot, TabListWrapper, TabList, Tab, TabIndicator, TabPanel } from "@heroui/react"
|
||||
import { TabsRoot, TabListContainer, TabList, Tab, TabIndicator, TabPanel } from "@heroui/react"
|
||||
|
||||
<TabsRoot>
|
||||
<TabListWrapper>
|
||||
<TabListContainer>
|
||||
<TabList>
|
||||
<Tab id="tab1">Tab 1<TabIndicator /></Tab>
|
||||
<Tab id="tab2">Tab 2<TabIndicator /></Tab>
|
||||
</TabList>
|
||||
</TabListWrapper>
|
||||
</TabListContainer>
|
||||
<TabPanel id="tab1">Panel 1</TabPanel>
|
||||
<TabPanel id="tab2">Panel 2</TabPanel>
|
||||
</TabsRoot>
|
||||
|
||||
@@ -0,0 +1,866 @@
|
||||
---
|
||||
title: v3.0.0-beta.1
|
||||
description: Major redesign with new design system, 8 new components, and improved developer experience.
|
||||
---
|
||||
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span className="text-sm text-muted">November 6, 2025</span>
|
||||
</div>
|
||||
|
||||
This release introduces a comprehensive redesign of HeroUI v3, merging v2's beauty and animations with v3's simplicity. All components redesigned, 8 new components, and improved design system with better color tokens, shadows, and architecture.
|
||||
|
||||
## Installation
|
||||
|
||||
Update to the latest version:
|
||||
|
||||
<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
|
||||
<Tab value="npm">
|
||||
```bash
|
||||
npm i @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="pnpm">
|
||||
```bash
|
||||
pnpm add @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="yarn">
|
||||
```bash
|
||||
yarn add @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="bun">
|
||||
```bash
|
||||
bun add @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout type="info">
|
||||
**Using AI assistants?** Simply prompt "Hey Cursor, update HeroUI to the latest version" and your AI assistant will automatically compare versions and apply the necessary changes. Learn more about the [HeroUI MCP Server](/docs/ui-for-agents/mcp-server).
|
||||
</Callout>
|
||||
|
||||
## What's New
|
||||
|
||||
### New Design System
|
||||
|
||||
We've spent weeks crafting a new design system that merges the soul of HeroUI v2 with the simplicity of v3. Every component has been redesigned with attention to detail, smooth animations, and improved developer experience. The new design system is available in our [Figma Kit V3](https://www.figma.com/community/file/1546526812159103429/heroui-figma-kit-v3).
|
||||
|
||||
<VideoPlayer
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/videos/v3-beta-1.mp4"
|
||||
poster="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/videos/v3-beta-1-poster.png"
|
||||
height={430}
|
||||
/>
|
||||
|
||||
The redesign brings:
|
||||
- New color system that brings v3's vision to life and stands out for its uniqueness
|
||||
- Refined shadow system for better depth perception
|
||||
- New variables and tokens for better customization
|
||||
- Automatic `isOnSurface` support for form-based components
|
||||
- Enhanced border and spacing tokens
|
||||
- Better contrast and accessibility
|
||||
- Consistent component patterns across web and native
|
||||
|
||||
### New Components
|
||||
|
||||
This release introduces **8 new** essential components:
|
||||
|
||||
- **[Alert](#alert)**: Display important messages and notifications with status indicators.
|
||||
- **[Checkbox & CheckboxGroup](#checkbox-checkboxgroup)**: Select multiple items from a list.
|
||||
- **[InputOTP](#inputotp)**: One-time password input for authentication flows.
|
||||
- **[Listbox](#listbox)**: Display a list of options and allow single or multiple selection.
|
||||
- **[Select](#select)**: Dropdown selection component built on top of Listbox.
|
||||
- **[Slider](#slider)**: Select a value from a range with custom marks and labels.
|
||||
- **[Surface](#surface)**: Base surface component for creating elevated containers.
|
||||
|
||||
<span id="alert"></span>
|
||||
### Alert
|
||||
|
||||
<ComponentPreview
|
||||
name="alert-basic"
|
||||
/>
|
||||
|
||||
<span id="checkbox-checkboxgroup"></span>
|
||||
### Checkbox & CheckboxGroup
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-basic"
|
||||
/>
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-basic"
|
||||
/>
|
||||
|
||||
<span id="inputotp"></span>
|
||||
### InputOTP
|
||||
|
||||
<ComponentPreview
|
||||
name="input-otp-basic"
|
||||
/>
|
||||
|
||||
<span id="listbox"></span>
|
||||
### Listbox
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-default"
|
||||
/>
|
||||
|
||||
<span id="select"></span>
|
||||
### Select
|
||||
|
||||
<ComponentPreview
|
||||
name="select-default"
|
||||
/>
|
||||
|
||||
<span id="slider"></span>
|
||||
### Slider
|
||||
|
||||
<ComponentPreview
|
||||
name="slider-default"
|
||||
/>
|
||||
|
||||
<span id="surface"></span>
|
||||
### Surface
|
||||
|
||||
<ComponentPreview
|
||||
name="surface-variants"
|
||||
/>
|
||||
|
||||
### Improved Component APIs
|
||||
|
||||
Several components have been refined with better APIs:
|
||||
|
||||
- **Link**: Added `underline` and `underlineOffset` props for better customization
|
||||
|
||||
<ComponentPreview
|
||||
name="link-basic"
|
||||
/>
|
||||
|
||||
- **Card**: Improved variants and styling system
|
||||
|
||||
<ComponentPreview
|
||||
name="card-with-images"
|
||||
/>
|
||||
|
||||
- **Chip**: Enhanced with size variants and improved color system
|
||||
|
||||
<ComponentPreview
|
||||
name="chip-basic"
|
||||
/>
|
||||
|
||||
- **Switch**: Redesigned from the ground up with improved visual design and animations
|
||||
|
||||
<span id="switch"></span>
|
||||
<ComponentPreview
|
||||
name="switch-basic"
|
||||
/>
|
||||
|
||||
- **RadioGroup**: Redesigned from the ground up with better API and styling
|
||||
|
||||
<span id="radiogroup"></span>
|
||||
<ComponentPreview
|
||||
name="radio-group-basic"
|
||||
/>
|
||||
|
||||
### Flexible Component Patterns
|
||||
|
||||
HeroUI now supports flexible component syntax. Use compound patterns with or without `.Root`, or named exports - all three patterns work identically.
|
||||
|
||||
**Available patterns:**
|
||||
|
||||
```tsx
|
||||
import { Avatar } from "@heroui/react"
|
||||
|
||||
// 1. Compound pattern (no .Root needed) - recommended
|
||||
<Avatar>
|
||||
<Avatar.Image src="/avatar.jpg" alt="User" />
|
||||
<Avatar.Fallback>JD</Avatar.Fallback>
|
||||
</Avatar>
|
||||
|
||||
// 2. Compound pattern with .Root - still supported
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="/avatar.jpg" alt="User" />
|
||||
<Avatar.Fallback>JD</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
|
||||
// 3. Named exports
|
||||
import { AvatarRoot, AvatarImage, AvatarFallback } from "@heroui/react"
|
||||
|
||||
<AvatarRoot>
|
||||
<AvatarImage src="/avatar.jpg" alt="User" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</AvatarRoot>
|
||||
```
|
||||
|
||||
**Simple components** like Button work the same way:
|
||||
|
||||
```tsx
|
||||
import { Button } from "@heroui/react"
|
||||
|
||||
// No .Root needed
|
||||
<Button>Label</Button>
|
||||
|
||||
// Or with .Root
|
||||
<Button.Root>Label</Button.Root>
|
||||
|
||||
// Or named export
|
||||
import { ButtonRoot } from "@heroui/react"
|
||||
<ButtonRoot>Label</ButtonRoot>
|
||||
```
|
||||
|
||||
**You can mix compound and named exports** in the same component:
|
||||
|
||||
```tsx
|
||||
import { Avatar, AvatarFallback } from "@heroui/react"
|
||||
|
||||
<Avatar>
|
||||
<Avatar.Image src="/avatar.jpg" alt="User" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
This provides:
|
||||
- **Simpler API**: Main components no longer require `.Root` suffix
|
||||
- **Flexibility**: Choose between compound pattern, compound with `.Root`, or named exports
|
||||
- **Backward Compatibility**: The `.Root` pattern still works
|
||||
- **Naming Consistency**: Standardized naming (e.g., "Container" instead of "Wrapper")
|
||||
|
||||
### Global Animation Control
|
||||
|
||||
HeroUI now supports easy global animation control through the `data-reduce-motion` attribute. Simply add `data-reduce-motion="true"` to your `<html>` or `<body>` tag to disable all animations across your application.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html data-reduce-motion="true">
|
||||
<!-- All HeroUI animations will be disabled -->
|
||||
</html>
|
||||
```
|
||||
|
||||
HeroUI automatically respects user motion preferences using the `prefers-reduced-motion` media query and extends Tailwind's `motion-reduce:` variant to support both system preferences and manual control via the data attribute. This provides flexible control over animations while maintaining accessibility best practices.
|
||||
|
||||
Learn more about animations and motion preferences in the [Animation documentation](/docs/handbook/animation).
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
|
||||
### Design System Variables
|
||||
|
||||
#### Panel → Surface & Overlay
|
||||
|
||||
The `--panel` variable has been replaced with `--surface` and `--overlay` to better distinguish between non-overlay components (cards, accordions) and floating components (tooltips, popovers, modals).
|
||||
|
||||
**Before:**
|
||||
```css
|
||||
--panel: var(--white);
|
||||
--panel-foreground: var(--foreground);
|
||||
--shadow-panel: 0 0 1px 0 rgba(0, 0, 0, 0.3) inset, 0 2px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
```
|
||||
|
||||
**After:**
|
||||
```css
|
||||
--surface: var(--white);
|
||||
--surface-foreground: var(--foreground);
|
||||
--overlay: var(--white);
|
||||
--overlay-foreground: var(--foreground);
|
||||
--shadow-surface: 0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
--shadow-overlay: 0 4px 16px 0 rgba(24, 24, 27, 0.08), 0 8px 24px 0 rgba(24, 24, 27, 0.09);
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
- Replace `bg-panel` with `bg-surface` for non-overlay components
|
||||
- Replace `bg-panel` with `bg-overlay` for floating components
|
||||
- Replace `shadow-panel` with `shadow-surface` or `shadow-overlay`
|
||||
- Replace `--color-panel` with `--color-surface` or `--color-overlay`
|
||||
|
||||
#### Surface Levels Simplified
|
||||
|
||||
The `--surface-1`, `--surface-2`, and `--surface-3` variables have been removed. Surface levels are now automatically calculated from `--surface` using `color-mix`, so you only need to declare the base surface color.
|
||||
|
||||
**Before (manual declaration):**
|
||||
```css
|
||||
--surface-1: var(--background);
|
||||
--surface-2: var(--color-neutral-100);
|
||||
--surface-3: var(--color-neutral-200);
|
||||
```
|
||||
|
||||
**After (auto-calculated):**
|
||||
```css
|
||||
/* You only declare the base surface */
|
||||
--surface: var(--white);
|
||||
--surface-foreground: var(--foreground);
|
||||
|
||||
/* HeroUI automatically calculates these using color-mix */
|
||||
--color-surface-secondary: color-mix(in oklab, var(--surface) 94%, var(--surface-foreground) 6%);
|
||||
--color-surface-tertiary: color-mix(in oklab, var(--surface) 92%, var(--surface-foreground) 8%);
|
||||
--color-surface-quaternary: color-mix(in oklab, var(--surface) 86%, var(--surface-foreground) 14%);
|
||||
```
|
||||
|
||||
**Customization:**
|
||||
|
||||
You can override the default calculations using Tailwind's `@theme` directive:
|
||||
|
||||
```css
|
||||
@theme inline {
|
||||
--color-surface-secondary: color-mix(in oklab, var(--surface) 96%, var(--surface-foreground) 4%);
|
||||
--color-surface-tertiary: color-mix(in oklab, var(--surface) 94%, var(--surface-foreground) 6%);
|
||||
--color-surface-quaternary: color-mix(in oklab, var(--surface) 90%, var(--surface-foreground) 10%);
|
||||
}
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
- Replace `bg-surface-1` with `bg-surface` (base surface)
|
||||
- Replace `bg-surface-2` with `bg-surface-secondary` (auto-calculated)
|
||||
- Replace `bg-surface-3` with `bg-surface-tertiary` (auto-calculated)
|
||||
|
||||
The same auto-calculation pattern applies to:
|
||||
- **Background shades**: Calculated from `--background` → `background-secondary`, `background-tertiary`, `background-quaternary`
|
||||
- **Soft colors**: Calculated from status colors → `accent-soft`, `danger-soft`, `warning-soft`, `success-soft`
|
||||
|
||||
#### Border Width Default Changed
|
||||
|
||||
The default border width has changed from `1px` to `0px`. Borders are now opt-in rather than default.
|
||||
|
||||
**Before:**
|
||||
```css
|
||||
--border-width: 1px;
|
||||
```
|
||||
|
||||
**After:**
|
||||
```css
|
||||
--border-width: 0px; /* no border by default */
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
- If you rely on default borders, explicitly set `border-width` in your custom styles
|
||||
- Form fields now use `transparent` borders by default
|
||||
|
||||
#### Border Color Default Changed
|
||||
|
||||
The default border color opacity has changed from `15%` to `0%` (transparent).
|
||||
|
||||
**Before:**
|
||||
```css
|
||||
--border: oklch(0 0 0 / 15%);
|
||||
```
|
||||
|
||||
**After:**
|
||||
```css
|
||||
--border: oklch(0 0 0 / 0%);
|
||||
```
|
||||
|
||||
**Field Border Default:**
|
||||
```css
|
||||
--field-border: transparent; /* no border by default on form fields */
|
||||
```
|
||||
|
||||
#### Shadow System Updates
|
||||
|
||||
The shadow system has been completely redesigned with separate shadows for surfaces and overlays.
|
||||
|
||||
**Before:**
|
||||
```css
|
||||
--panel-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.3) inset, 0 2px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
--field-shadow: 0 0 0 0 rgba(255, 255, 255, 0.1) inset, 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
```
|
||||
|
||||
**After (Light):**
|
||||
```css
|
||||
--surface-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
--overlay-shadow: 0 4px 16px 0 rgba(24, 24, 27, 0.08), 0 8px 24px 0 rgba(24, 24, 27, 0.09);
|
||||
--field-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06), 0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
```
|
||||
|
||||
**After (Dark):**
|
||||
```css
|
||||
--surface-shadow: 0 0 0 0 transparent inset; /* No shadow on dark mode */
|
||||
--overlay-shadow: 0 0 0 0 transparent inset; /* No shadow on dark mode */
|
||||
--field-shadow: 0 0 0 0 transparent inset; /* Transparent shadow to allow ring utilities to work */
|
||||
```
|
||||
|
||||
#### Accent Color Updates
|
||||
|
||||
The accent color has been updated for better contrast and visual appeal.
|
||||
|
||||
**Before:**
|
||||
```css
|
||||
--accent: var(--color-neutral-950);
|
||||
--accent-foreground: var(--snow);
|
||||
```
|
||||
|
||||
**After:**
|
||||
```css
|
||||
--accent: oklch(0.6204 0.195 253.83);
|
||||
--accent-foreground: var(--snow);
|
||||
```
|
||||
|
||||
#### Status Color Refinements
|
||||
|
||||
Success, warning, and danger colors have been refined for better consistency and contrast.
|
||||
|
||||
**Success:**
|
||||
- **Before:** `oklch(0.5503 0.1244 153.56)`
|
||||
- **After:** `oklch(0.7329 0.1935 150.81)`
|
||||
- Foreground changed from `var(--snow)` to `var(--eclipse)` in light mode
|
||||
|
||||
**Warning:**
|
||||
- **Before:** `oklch(0.7186 0.1521 64.85)`
|
||||
- **After:** `oklch(0.7819 0.1585 72.33)` (light), `oklch(0.8203 0.1388 76.34)` (dark)
|
||||
|
||||
**Danger:**
|
||||
- **Before:** `oklch(0.6259 0.1908 29.19)`
|
||||
- **After:** `oklch(0.6532 0.2328 25.74)` (light), `oklch(0.594 0.1967 24.63)` (dark)
|
||||
|
||||
### Component API Changes
|
||||
|
||||
#### Chip Component
|
||||
|
||||
The Chip component's `type` prop has been renamed to `color`, and a new `size` prop has been added. A new `soft` variant has been introduced.
|
||||
|
||||
**Before:**
|
||||
```tsx
|
||||
import { Chip } from "@heroui/react";
|
||||
|
||||
<Chip type="danger" variant="secondary">Label</Chip>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```tsx
|
||||
import { Chip } from "@heroui/react";
|
||||
|
||||
<Chip color="danger" variant="soft" size="md">Label</Chip>
|
||||
```
|
||||
|
||||
**Migration:**
|
||||
- Replace `type` prop with `color` prop
|
||||
- Use `size` prop (`sm`, `md`, `lg`) to control chip size
|
||||
- The `soft` variant provides a subtle appearance for less prominent chips
|
||||
|
||||
#### Link Component
|
||||
|
||||
The Link component now supports `underline` and `underlineOffset` props, and includes `asChild` support.
|
||||
|
||||
**Before:**
|
||||
```tsx
|
||||
import { Link } from "@heroui/react";
|
||||
|
||||
<Link href="#">Link text</Link>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```tsx
|
||||
import { Link } from "@heroui/react";
|
||||
|
||||
<Link href="#" underline="hover" underlineOffset={4}>Link text</Link>
|
||||
```
|
||||
|
||||
**New Props:**
|
||||
- `underline`: `"none" | "hover" | "always"` - Controls underline visibility
|
||||
- `underlineOffset`: `number` - Controls underline offset from text
|
||||
|
||||
#### Type Reference Syntax
|
||||
|
||||
Due to the dual pattern implementation, type references through the namespace syntax are no longer supported. Use object-style syntax or named type imports instead.
|
||||
|
||||
**Before (no longer works):**
|
||||
```tsx
|
||||
type AvatarProps = Avatar.RootProps
|
||||
```
|
||||
|
||||
**After (Option 1 - Object-style syntax):**
|
||||
```tsx
|
||||
type AvatarProps = Avatar["RootProps"]
|
||||
```
|
||||
|
||||
**After (Option 2 - Named type imports, recommended):**
|
||||
```tsx
|
||||
import type { AvatarRootProps } from "@heroui/react"
|
||||
|
||||
type AvatarProps = AvatarRootProps
|
||||
```
|
||||
|
||||
This change affects all compound components when accessing prop types.
|
||||
|
||||
#### Tabs Component Renaming
|
||||
|
||||
The Tabs component's wrapper element has been renamed for consistency:
|
||||
|
||||
- **Compound property**: `Tabs.ListWrapper` → `Tabs.ListContainer`
|
||||
- **Named export**: `TabListWrapper` → `TabListContainer`
|
||||
- **CSS class**: `.tabs__list-wrapper` → `.tabs__list-container`
|
||||
- **Data attribute**: `data-slot="tabs-list-wrapper"` → `data-slot="tabs-list-container"`
|
||||
|
||||
**Migration:**
|
||||
|
||||
Find and replace all instances of `TabListWrapper` with `TabListContainer`:
|
||||
|
||||
```bash
|
||||
# Component usage
|
||||
TabListWrapper → TabListContainer
|
||||
Tabs.ListWrapper → Tabs.ListContainer
|
||||
|
||||
# CSS selectors (if using custom styles)
|
||||
.tabs__list-wrapper → .tabs__list-container
|
||||
[data-slot="tabs-list-wrapper"] → [data-slot="tabs-list-container"]
|
||||
```
|
||||
|
||||
#### Removed Variables
|
||||
|
||||
The following variables have been removed:
|
||||
- `--panel` → Use `--surface` or `--overlay`
|
||||
- `--panel-foreground` → Use `--surface-foreground` or `--overlay-foreground`
|
||||
- `--surface-1`, `--surface-2`, `--surface-3` → Use background shades or surface levels
|
||||
- `--accent-soft` → Use `--color-accent-soft` (now calculated)
|
||||
- `--radius-panel` and `--radius-panel-inner` → Use standard radius values
|
||||
|
||||
## Design System Updates
|
||||
|
||||
### New Color System
|
||||
|
||||
#### Surface vs Overlay Concept
|
||||
|
||||
The design system now distinguishes between two types of elevated components:
|
||||
|
||||
- **Surface**: Used for non-overlay components like cards, accordions, and disclosure groups that sit on the page
|
||||
- **Overlay**: Used for floating components like tooltips, popovers, modals, and menus that appear above the page
|
||||
|
||||
This distinction provides:
|
||||
- Better visual hierarchy
|
||||
- Appropriate shadow depths
|
||||
- Improved dark mode contrast
|
||||
- Clearer component semantics
|
||||
|
||||
#### Auto-Calculated Color System
|
||||
|
||||
HeroUI now automatically calculates shade levels and soft color variants using CSS `color-mix`. You only need to declare the base colors, and HeroUI handles the rest.
|
||||
|
||||
**Background Shade Levels**
|
||||
|
||||
Background shades are automatically calculated from `--background`:
|
||||
|
||||
```css
|
||||
/* You only declare the base */
|
||||
--background: oklch(0.9702 0 0);
|
||||
--foreground: var(--eclipse);
|
||||
|
||||
/* HeroUI automatically calculates these */
|
||||
--color-background-secondary: color-mix(in oklab, var(--color-background) 96%, var(--color-foreground) 4%);
|
||||
--color-background-tertiary: color-mix(in oklab, var(--color-background) 92%, var(--color-foreground) 8%);
|
||||
--color-background-quaternary: color-mix(in oklab, var(--color-background) 86%, var(--color-foreground) 14%);
|
||||
```
|
||||
|
||||
**Surface Levels**
|
||||
|
||||
Surface levels are automatically calculated from `--surface`:
|
||||
|
||||
```css
|
||||
/* You only declare the base */
|
||||
--surface: var(--white);
|
||||
--surface-foreground: var(--foreground);
|
||||
|
||||
/* HeroUI automatically calculates these */
|
||||
--color-surface-secondary: color-mix(in oklab, var(--surface) 94%, var(--surface-foreground) 6%);
|
||||
--color-surface-tertiary: color-mix(in oklab, var(--surface) 92%, var(--surface-foreground) 8%);
|
||||
--color-surface-quaternary: color-mix(in oklab, var(--surface) 86%, var(--surface-foreground) 14%);
|
||||
```
|
||||
|
||||
**Soft Color Variants**
|
||||
|
||||
Soft color variants are automatically calculated from status colors:
|
||||
|
||||
```css
|
||||
/* You declare the base status colors */
|
||||
--accent: oklch(0.6204 0.195 253.83);
|
||||
--danger: oklch(0.6532 0.2328 25.74);
|
||||
--warning: oklch(0.7819 0.1585 72.33);
|
||||
--success: oklch(0.7329 0.1935 150.81);
|
||||
|
||||
/* HeroUI automatically calculates these at 15% opacity */
|
||||
--color-accent-soft: color-mix(in oklab, var(--color-accent) 15%, transparent);
|
||||
--color-danger-soft: color-mix(in oklab, var(--color-danger) 15%, transparent);
|
||||
--color-warning-soft: color-mix(in oklab, var(--color-warning) 15%, transparent);
|
||||
--color-success-soft: color-mix(in oklab, var(--color-success) 15%, transparent);
|
||||
```
|
||||
|
||||
Each soft variant includes hover states (20% opacity) and foreground colors for proper contrast.
|
||||
|
||||
**Customization:**
|
||||
|
||||
You can override any auto-calculated values using Tailwind's `@theme` directive:
|
||||
|
||||
```css
|
||||
@theme inline {
|
||||
/* Adjust surface levels */
|
||||
--color-surface-secondary: color-mix(in oklab, var(--surface) 96%, var(--surface-foreground) 4%);
|
||||
|
||||
/* Adjust soft colors */
|
||||
--color-accent-soft: color-mix(in oklab, var(--color-accent) 20%, transparent);
|
||||
}
|
||||
```
|
||||
|
||||
This auto-calculation system reduces the number of variables you need to manage while providing full customization when needed.
|
||||
|
||||
### Shadow System
|
||||
|
||||
The shadow system has been redesigned to provide:
|
||||
- Separate shadows for surfaces and overlays
|
||||
- Better depth perception
|
||||
- Dark mode support (transparent shadows)
|
||||
- Consistent field shadows
|
||||
|
||||
Shadows automatically adapt to light and dark modes, providing appropriate depth cues for each theme.
|
||||
|
||||
### Focus System
|
||||
|
||||
The focus color now uses the accent color for consistency:
|
||||
|
||||
```css
|
||||
--focus: var(--accent);
|
||||
```
|
||||
|
||||
This ensures focus indicators align with your brand colors while maintaining accessibility.
|
||||
|
||||
### Typography Tokens
|
||||
|
||||
Several typography-related variables have been removed in favor of using Tailwind's typography utilities directly. The design system now focuses on color and spacing tokens, letting Tailwind handle typography.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Step 1: Update Design System Variables
|
||||
|
||||
Replace old panel variables with surface/overlay:
|
||||
|
||||
```css
|
||||
/* Before */
|
||||
.my-card {
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow-panel);
|
||||
}
|
||||
|
||||
/* After */
|
||||
.my-card {
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-surface);
|
||||
}
|
||||
|
||||
.my-tooltip {
|
||||
background: var(--overlay);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Update Surface Levels
|
||||
|
||||
Surface levels are now automatically calculated from `--surface`, so you don't need to manually declare them. Simply use the new utility classes:
|
||||
|
||||
```css
|
||||
/* Before */
|
||||
.bg-surface-1 → .bg-surface (base surface)
|
||||
.bg-surface-2 → .bg-surface-secondary (auto-calculated)
|
||||
.bg-surface-3 → .bg-surface-tertiary (auto-calculated)
|
||||
|
||||
/* You can also use background shades */
|
||||
.bg-surface-2 → .bg-background-secondary (auto-calculated from --background)
|
||||
.bg-surface-3 → .bg-background-tertiary (auto-calculated from --background)
|
||||
```
|
||||
|
||||
**Note:** Surface levels (`surface-secondary`, `surface-tertiary`, etc.) are automatically calculated based on your `--surface` color. No manual CSS variables needed unless you want to customize the calculations.
|
||||
|
||||
### Step 3: Update Component Props
|
||||
|
||||
Update Chip and Link components:
|
||||
|
||||
```tsx
|
||||
// Chip: type → color, add size if needed
|
||||
<Chip type="danger" /> → <Chip color="danger" size="md" />
|
||||
|
||||
// Link: Add underline props if customizing underlines
|
||||
<Link href="#">Text</Link> // Still works, underline props are optional
|
||||
```
|
||||
|
||||
### Step 4: Simplify Component Patterns (Optional)
|
||||
|
||||
If you adopted the `.Root` suffix from v3.0.0-alpha.35, you can now simplify your code by removing it:
|
||||
|
||||
**Before (v3.0.0-alpha.35):**
|
||||
```tsx
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src="..." alt="..." />
|
||||
<Avatar.Fallback>JD</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
```
|
||||
|
||||
**After (simpler):**
|
||||
```tsx
|
||||
<Avatar>
|
||||
<Avatar.Image src="..." alt="..." />
|
||||
<Avatar.Fallback>JD</Avatar.Fallback>
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
**Note:** The `.Root` syntax still works if you prefer it.
|
||||
|
||||
### Step 5: Update Type References
|
||||
|
||||
If you're using namespace syntax for types, switch to object-style syntax or named imports:
|
||||
|
||||
**Before:**
|
||||
```tsx
|
||||
type ButtonProps = Button.RootProps
|
||||
```
|
||||
|
||||
**After (Option 1 - Object-style):**
|
||||
```tsx
|
||||
type ButtonProps = Button["RootProps"]
|
||||
```
|
||||
|
||||
**After (Option 2 - Named imports, recommended):**
|
||||
```tsx
|
||||
import type { ButtonRootProps } from "@heroui/react"
|
||||
|
||||
type ButtonProps = ButtonRootProps
|
||||
```
|
||||
|
||||
### Step 6: Update Tabs Component
|
||||
|
||||
Replace `TabListWrapper` with `TabListContainer`:
|
||||
|
||||
**Before:**
|
||||
```tsx
|
||||
import { Tabs } from "@heroui/react"
|
||||
|
||||
<Tabs.Root>
|
||||
<Tabs.ListWrapper>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab id="home">Home<Tabs.Indicator /></Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListWrapper>
|
||||
<Tabs.Panel id="home">Content</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
```
|
||||
|
||||
**After:**
|
||||
```tsx
|
||||
import { Tabs } from "@heroui/react"
|
||||
|
||||
<Tabs>
|
||||
<Tabs.ListContainer>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab id="home">Home<Tabs.Indicator /></Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListContainer>
|
||||
<Tabs.Panel id="home">Content</Tabs.Panel>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
### Step 7: Handle Border Changes
|
||||
|
||||
If your custom styles rely on default borders:
|
||||
|
||||
```css
|
||||
/* Add explicit borders where needed */
|
||||
.my-component {
|
||||
border-width: 1px;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8: Update Status Colors
|
||||
|
||||
If you've customized status colors, review the new values and adjust your custom theme if needed:
|
||||
|
||||
```css
|
||||
/* Check if your custom status colors need updates */
|
||||
--success: oklch(0.7329 0.1935 150.81); /* New value */
|
||||
--warning: oklch(0.7819 0.1585 72.33); /* New value */
|
||||
--danger: oklch(0.6532 0.2328 25.74); /* New value */
|
||||
```
|
||||
|
||||
### Automated Migration
|
||||
|
||||
For large codebases, you can use find-and-replace:
|
||||
|
||||
```bash
|
||||
# Panel → Surface
|
||||
--panel → --surface
|
||||
bg-panel → bg-surface
|
||||
shadow-panel → shadow-surface
|
||||
|
||||
# Panel → Overlay (for floating components)
|
||||
--panel → --overlay (where appropriate)
|
||||
bg-panel → bg-overlay (for tooltips, popovers, etc.)
|
||||
shadow-panel → shadow-overlay (for floating components)
|
||||
|
||||
# Chip type prop
|
||||
type=" → color="
|
||||
|
||||
# Surface levels
|
||||
bg-surface-1 → bg-surface
|
||||
bg-surface-2 → bg-surface-secondary
|
||||
bg-surface-3 → bg-surface-tertiary
|
||||
|
||||
# Tabs component
|
||||
TabListWrapper → TabListContainer
|
||||
Tabs.ListWrapper → Tabs.ListContainer
|
||||
|
||||
# Type references
|
||||
Component.RootProps → Component["RootProps"] or use named imports
|
||||
```
|
||||
|
||||
## Component Updates
|
||||
|
||||
### Card Component
|
||||
|
||||
Card component has been refined with improved variants and better semantic structure. The component now uses the new surface system for consistent styling.
|
||||
|
||||
### Accordion Component
|
||||
|
||||
Accordion now uses the surface system for better visual consistency with other components.
|
||||
|
||||
### Form Components
|
||||
|
||||
Form components (Input, TextField, TextArea) have been updated to use the new field border system (transparent by default) for a cleaner look while maintaining accessibility.
|
||||
|
||||
### Component Pattern Updates
|
||||
|
||||
All components now support flexible patterns. Components that support the dual pattern include:
|
||||
|
||||
- **Simple components**: Button, Link, Spinner, Chip, Kbd
|
||||
- **Compound components**: Accordion, Avatar, Card, Disclosure, Fieldset, Popover, RadioGroup, Switch, Tabs, Tooltip
|
||||
|
||||
You can use any of the three patterns (compound without `.Root`, compound with `.Root`, or named exports) with all these components.
|
||||
|
||||
## HeroUI Pro
|
||||
|
||||
HeroUI Pro is being reshaped from the ground up on top of the new design system. The new Pro version will feature:
|
||||
- New components built on top of HeroUI v3
|
||||
- Tailwind CSS v4 native support
|
||||
- CSS native animations
|
||||
- Enhanced customization options
|
||||
|
||||
We'll share more updates soon.
|
||||
|
||||
## Roadmap
|
||||
|
||||
We're working towards a stable release in **Q4** this year (2025). This beta release brings us significantly closer to that goal with:
|
||||
- Comprehensive component set
|
||||
- Refined design system
|
||||
- Improved developer experience
|
||||
- Better performance
|
||||
|
||||
## Community
|
||||
|
||||
The reception on the native side has been phenomenal. Thank you for supporting us as we build HeroUI v3! Your feedback helps us improve every day.
|
||||
|
||||
See what the community is saying: [HeroUI Native Reception](https://x.com/hero_ui/status/1985721976220966926)
|
||||
|
||||
## Links
|
||||
|
||||
- [Component Documentation](https://v3.heroui.com/docs/components)
|
||||
- [Design System - Figma Kit V3](https://www.figma.com/community/file/1546526812159103429/heroui-figma-kit-v3)
|
||||
- [HeroUI Native](https://link.heroui.com/native)
|
||||
- [GitHub Repository](https://github.com/heroui-inc/heroui)
|
||||
- [GitHub PR #5872](https://github.com/heroui-inc/heroui/pull/5872)
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed to this release, helping us create a design system that's both beautiful and practical!
|
||||
|
||||
@@ -6,7 +6,7 @@ links:
|
||||
rac: Disclosure
|
||||
source: accordion/accordion.tsx
|
||||
styles: accordion.css
|
||||
storybook: accordion
|
||||
storybook: Components/Navigation/Accordion
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -31,7 +31,7 @@ Import the Accordion component and access all parts using dot notation.
|
||||
import { Accordion } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Accordion.Root>
|
||||
<Accordion>
|
||||
<Accordion.Item>
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger>
|
||||
@@ -42,7 +42,7 @@ export default () => (
|
||||
<Accordion.Body/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
</Accordion>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -120,7 +120,7 @@ const items = [
|
||||
|
||||
export function CustomStyles() {
|
||||
return (
|
||||
<Accordion.Root className="bg-surface-1 w-full max-w-md rounded-2xl" variant="outline">
|
||||
<Accordion className="bg-surface-secondary w-full max-w-md rounded-2xl" variant="surface">
|
||||
{items.map((item, index) => (
|
||||
<Accordion.Item
|
||||
key={index}
|
||||
@@ -131,7 +131,7 @@ export function CustomStyles() {
|
||||
)}
|
||||
>
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger className="hover:bg-surface-2 group flex items-center gap-2">
|
||||
<Accordion.Trigger className="hover:bg-surface-tertiary group flex items-center gap-2">
|
||||
{item.iconUrl ? (
|
||||
<img
|
||||
alt={item.title}
|
||||
@@ -153,7 +153,7 @@ export function CustomStyles() {
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</Accordion>
|
||||
);
|
||||
}`} />
|
||||
|
||||
@@ -211,7 +211,7 @@ The component supports both CSS pseudo-classes and data attributes for flexibili
|
||||
|
||||
## API Reference
|
||||
|
||||
### Accordion.Root Props
|
||||
### Accordion Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
@@ -220,7 +220,7 @@ The component supports both CSS pseudo-classes and data attributes for flexibili
|
||||
| `expandedKeys` | `Iterable<Key>` | - | The controlled expanded keys |
|
||||
| `onExpandedChange` | `(keys: Set<Key>) => void` | - | Handler called when expanded keys change |
|
||||
| `isDisabled` | `boolean` | `false` | Whether the entire accordion is disabled |
|
||||
| `variant` | `"default" \| "outline"` | `"default"` | The visual variant of the accordion |
|
||||
| `variant` | `"default" \| "surface"` | `"default"` | The visual variant of the accordion |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | The accordion items |
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
title: Alert
|
||||
description: Display important messages and notifications to users with status indicators
|
||||
icon: new
|
||||
links:
|
||||
source: alert/alert.tsx
|
||||
styles: alert.css
|
||||
storybook: Components/Feedback/Alert
|
||||
figma: true
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="alert-basic"
|
||||
/>
|
||||
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the Alert component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Alert>
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Title />
|
||||
<Alert.Description />
|
||||
</Alert.Content>
|
||||
</Alert>
|
||||
)
|
||||
```
|
||||
|
||||
<RelatedComponents component="alert" />
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import { Alert } from "@heroui/react";
|
||||
|
||||
function CustomAlert() {
|
||||
return (
|
||||
<Alert className="border-2 border-blue-500 rounded-xl" status="accent">
|
||||
<Alert.Indicator className="text-blue-600" />
|
||||
<Alert.Content className="gap-1">
|
||||
<Alert.Title className="font-bold text-lg">Custom Alert</Alert.Title>
|
||||
<Alert.Description className="text-sm opacity-80">
|
||||
This alert has custom styling applied
|
||||
</Alert.Description>
|
||||
</Alert.Content>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the Alert component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.alert {
|
||||
@apply rounded-2xl shadow-lg;
|
||||
}
|
||||
|
||||
.alert__title {
|
||||
@apply font-bold text-lg;
|
||||
}
|
||||
|
||||
.alert--danger {
|
||||
@apply border-l-4 border-red-600;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The Alert component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/alert.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.alert` - Base alert container
|
||||
- `.alert__indicator` - Icon/indicator container
|
||||
- `.alert__content` - Content wrapper for title and description
|
||||
- `.alert__title` - Alert title text
|
||||
- `.alert__description` - Alert description text
|
||||
|
||||
#### Status Variant Classes
|
||||
- `.alert--default` - Default gray status
|
||||
- `.alert--accent` - Accent blue status
|
||||
- `.alert--success` - Success green status
|
||||
- `.alert--warning` - Warning yellow/orange status
|
||||
- `.alert--danger` - Danger red status
|
||||
|
||||
### Interactive States
|
||||
|
||||
The Alert component is primarily informational and doesn't have interactive states on the base component. However, it can contain interactive elements like buttons or close buttons.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Alert Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `status` | `"default" \| "accent" \| "success" \| "warning" \| "danger"` | `"default"` | The visual status of the alert |
|
||||
| `asChild` | `boolean` | `false` | Merge props onto the child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | The alert content |
|
||||
|
||||
### Alert.Indicator Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `asChild` | `boolean` | `false` | Merge props onto the child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | Custom indicator icon (defaults to status icon) |
|
||||
|
||||
### Alert.Content Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `asChild` | `boolean` | `false` | Merge props onto the child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | Content (typically Title and Description) |
|
||||
|
||||
### Alert.Title Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `asChild` | `boolean` | `false` | Merge props onto the child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | The alert title text |
|
||||
|
||||
### Alert.Description Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `asChild` | `boolean` | `false` | Merge props onto the child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | The alert description text |
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Avatar
|
||||
description: Display user profile images with customizable fallback content
|
||||
icon: updated
|
||||
links:
|
||||
radix: avatar
|
||||
source: avatar/avatar.tsx
|
||||
styles: avatar.css
|
||||
storybook: avatar
|
||||
storybook: Components/Media/Avatar
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -28,10 +27,10 @@ Import the Avatar component and access all parts using dot notation.
|
||||
import { Avatar } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Avatar.Root>
|
||||
<Avatar>
|
||||
<Avatar.Image/>
|
||||
<Avatar.Fallback/>
|
||||
</Avatar.Root>
|
||||
</Avatar>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -43,6 +42,10 @@ export default () => (
|
||||
|
||||
<ComponentPreview name="avatar-colors" />
|
||||
|
||||
### Variants
|
||||
|
||||
<ComponentPreview name="avatar-variants" />
|
||||
|
||||
### Fallback Content
|
||||
|
||||
<ComponentPreview name="avatar-fallback" />
|
||||
@@ -64,10 +67,10 @@ import { Avatar } from '@heroui/react';
|
||||
|
||||
function CustomAvatar() {
|
||||
return (
|
||||
<Avatar.Root className="size-20">
|
||||
<Avatar className="size-20">
|
||||
<Avatar.Image src="..." alt="..." />
|
||||
<Avatar.Fallback>XL</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -105,6 +108,9 @@ The Avatar component uses these CSS classes ([View source styles](https://github
|
||||
- `.avatar--md` - Medium avatar (default, no additional styles)
|
||||
- `.avatar--lg` - Large avatar (size-12)
|
||||
|
||||
#### Variant Modifiers
|
||||
- `.avatar--soft` - Soft variant with lighter background
|
||||
|
||||
#### Color Modifiers
|
||||
- `.avatar__fallback--default` - Default text color
|
||||
- `.avatar__fallback--accent` - Accent text color
|
||||
@@ -114,12 +120,13 @@ The Avatar component uses these CSS classes ([View source styles](https://github
|
||||
|
||||
## API Reference
|
||||
|
||||
### Avatar.Root Props
|
||||
### Avatar Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Avatar size |
|
||||
| `color` | `'default' \| 'accent' \| 'success' \| 'warning' \| 'danger'` | `'default'` | Fallback color theme |
|
||||
| `variant` | `'default' \| 'soft'` | `'default'` | Visual style variant |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `asChild` | `boolean` | `false` | Render as child element |
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ links:
|
||||
rac: Button
|
||||
source: button/button.tsx
|
||||
styles: button.css
|
||||
storybook: button
|
||||
storybook: Components/Buttons/Button
|
||||
figma: true
|
||||
---
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: updated
|
||||
links:
|
||||
source: card/card.tsx
|
||||
styles: card.css
|
||||
storybook: card
|
||||
storybook: Components/Layout/Card
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -28,23 +28,31 @@ Import the Card component and access all parts using dot notation.
|
||||
import { Card } from "@heroui/react";
|
||||
|
||||
export default () => (
|
||||
<Card.Root>
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<Card.Title />
|
||||
<Card.Description />
|
||||
</Card.Header>
|
||||
<Card.Content />
|
||||
<Card.Footer />
|
||||
</Card.Root>
|
||||
</Card>
|
||||
);
|
||||
```
|
||||
|
||||
### Variants
|
||||
|
||||
Cards come in semantic variants that describe their prominence level rather than specific visual styles. This allows themes to interpret them differently:
|
||||
|
||||
<ComponentPreview
|
||||
name="card-variants"
|
||||
/>
|
||||
|
||||
- **`transparent`** - Minimal prominence, transparent background (great for nested cards)
|
||||
- **`default`** - Standard card for most use cases (surface-secondary)
|
||||
- **`secondary`** - Medium prominence to draw moderate attention (surface-tertiary)
|
||||
- **`tertiary`** - Higher prominence for important content (surface-quaternary)
|
||||
- **`quaternary`** - Highest prominence for critical content
|
||||
|
||||
### Horizontal Layout
|
||||
|
||||
<ComponentPreview
|
||||
@@ -57,16 +65,10 @@ export default () => (
|
||||
name="card-with-avatar"
|
||||
/>
|
||||
|
||||
### With Image
|
||||
### With Images
|
||||
|
||||
<ComponentPreview
|
||||
name="card-with-image"
|
||||
/>
|
||||
|
||||
### With Background Image
|
||||
|
||||
<ComponentPreview
|
||||
name="card-with-background-image"
|
||||
name="card-with-images"
|
||||
/>
|
||||
|
||||
### With Form
|
||||
@@ -79,18 +81,18 @@ export default () => (
|
||||
|
||||
```tsx
|
||||
// Semantic markup
|
||||
<Card.Root role="article" aria-labelledby="card-title">
|
||||
<Card role="article" aria-labelledby="card-title">
|
||||
<Card.Header>
|
||||
<Card.Title id="card-title">Article Title</Card.Title>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
</Card>
|
||||
|
||||
// Interactive cards
|
||||
<Card.Root asChild>
|
||||
<Card asChild>
|
||||
<a href="/details" aria-label="View product details">
|
||||
<Card.Title>Product Name</Card.Title>
|
||||
</a>
|
||||
</Card.Root>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Styling
|
||||
@@ -98,7 +100,7 @@ export default () => (
|
||||
### Component Customization
|
||||
|
||||
```tsx
|
||||
<Card.Root className="border-2 border-blue-500 bg-gradient-to-r from-blue-50 to-purple-50">
|
||||
<Card className="border-2 border-blue-500 bg-gradient-to-r from-blue-50 to-purple-50">
|
||||
<Card.Header>
|
||||
<Card.Title className="text-blue-900">Custom Styled Card</Card.Title>
|
||||
<Card.Description className="text-blue-700">Custom colors applied</Card.Description>
|
||||
@@ -106,14 +108,14 @@ export default () => (
|
||||
<Card.Content>
|
||||
<p className="text-blue-800">Content with custom styling</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### CSS Variable Overrides
|
||||
|
||||
```css
|
||||
/* Override specific variants */
|
||||
.card--variant-elevated {
|
||||
.card--secondary {
|
||||
@apply bg-gradient-to-br from-blue-50 to-purple-50;
|
||||
}
|
||||
|
||||
@@ -139,18 +141,19 @@ Card uses BEM naming for predictable styling, ([View source styles](https://gith
|
||||
|
||||
#### Variant Classes
|
||||
|
||||
- `.card--variant-flat` - Transparent background, no border
|
||||
- `.card--variant-outlined` - Border with subtle background (default)
|
||||
- `.card--variant-elevated` - Slightly elevated with medium background
|
||||
- `.card--variant-filled` - Prominently filled with stronger background
|
||||
- `.card--transparent` - Minimal prominence, transparent background (maps to `transparent` variant)
|
||||
- `.card--default` - Standard appearance with surface-secondary (default)
|
||||
- `.card--secondary` - Medium prominence with surface-tertiary (maps to `secondary` variant)
|
||||
- `.card--tertiary` - Higher prominence with surface-quaternary (maps to `tertiary` variant)
|
||||
- `.card--quaternary` - Highest prominence for critical content (maps to `quaternary` variant)
|
||||
|
||||
## API Reference
|
||||
|
||||
### Card.Root
|
||||
### Card
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ----------- | --------------------------------------------------------- | ------------ | -------------------------- |
|
||||
| `variant` | `"flat" \| "outlined" \| "elevated" \| "filled"` | `"outlined"` | Visual variant of the card |
|
||||
| `variant` | `"transparent" \| "default" \| "secondary" \| "tertiary" \| "quaternary"` | `"default"` | Semantic variant indicating prominence level |
|
||||
| `asChild` | `boolean` | `false` | Render as a child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `React.ReactNode` | - | Card content |
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: CheckboxGroup
|
||||
description: A checkbox group component for managing multiple checkbox selections
|
||||
icon: new
|
||||
links:
|
||||
rac: CheckboxGroup
|
||||
source: checkbox-group/checkbox-group.tsx
|
||||
styles: checkbox-group.css
|
||||
storybook: Components/Forms/CheckboxGroup
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { CheckboxGroup, Checkbox, Label, Description } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-basic"
|
||||
/>
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the CheckboxGroup component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import {CheckboxGroup, Checkbox, Label, Description, FieldError} from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<CheckboxGroup name="interests">
|
||||
<Label />
|
||||
<Description /> {/* Optional */}
|
||||
<Checkbox value="option1">
|
||||
<Checkbox.Control>
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox.Control>
|
||||
<Checkbox.Content>
|
||||
<Label />
|
||||
<Description /> {/* Optional */}
|
||||
</Checkbox.Content>
|
||||
</Checkbox>
|
||||
<FieldError /> {/* Optional */}
|
||||
</CheckboxGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, CheckboxGroup automatically applies on-surface styling.
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-on-surface"
|
||||
/>
|
||||
|
||||
### With Custom Indicator
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-with-custom-indicator"
|
||||
/>
|
||||
|
||||
### Indeterminate
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-indeterminate"
|
||||
/>
|
||||
|
||||
### Controlled
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-controlled"
|
||||
/>
|
||||
|
||||
### Validation
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-validation"
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-disabled"
|
||||
/>
|
||||
|
||||
### Features and Add-ons Example
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-group-features-and-addons"
|
||||
/>
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
You can customize the CheckboxGroup component:
|
||||
|
||||
```tsx
|
||||
import { CheckboxGroup, Checkbox, Label } from '@heroui/react';
|
||||
|
||||
function CustomCheckboxGroup() {
|
||||
return (
|
||||
<CheckboxGroup className="gap-4" name="custom">
|
||||
<Checkbox value="option1">
|
||||
<Checkbox.Control className="border-2 border-purple-500 data-[selected=true]:bg-purple-500">
|
||||
<Checkbox.Indicator className="text-white" />
|
||||
</Checkbox.Control>
|
||||
<Checkbox.Content>
|
||||
<Label>Option 1</Label>
|
||||
</Checkbox.Content>
|
||||
</Checkbox>
|
||||
</CheckboxGroup>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the CheckboxGroup component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.checkbox-group {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The CheckboxGroup component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/checkbox-group.css)):
|
||||
|
||||
- `.checkbox-group` - Base checkbox group container
|
||||
|
||||
## API Reference
|
||||
|
||||
### CheckboxGroup Props
|
||||
|
||||
Inherits from [React Aria CheckboxGroup](https://react-spectrum.adobe.com/react-aria/CheckboxGroup.html).
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `string[]` | - | The current selected values (controlled) |
|
||||
| `defaultValue` | `string[]` | - | The default selected values (uncontrolled) |
|
||||
| `onChange` | `(value: string[]) => void` | - | Handler called when the selected values change |
|
||||
| `isDisabled` | `boolean` | `false` | Whether the checkbox group is disabled |
|
||||
| `isRequired` | `boolean` | `false` | Whether the checkbox group is required |
|
||||
| `isReadOnly` | `boolean` | `false` | Whether the checkbox group is read only |
|
||||
| `isInvalid` | `boolean` | `false` | Whether the checkbox group is in an invalid state |
|
||||
| `name` | `string` | - | The name of the checkbox group, used when submitting an HTML form |
|
||||
| `children` | `React.ReactNode \| (values: CheckboxGroupRenderProps) => React.ReactNode` | - | Checkbox group content or render prop |
|
||||
|
||||
### CheckboxGroupRenderProps
|
||||
|
||||
When using the render prop pattern, these values are provided:
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `value` | `string[]` | The currently selected values |
|
||||
| `isDisabled` | `boolean` | Whether the checkbox group is disabled |
|
||||
| `isReadOnly` | `boolean` | Whether the checkbox group is read only |
|
||||
| `isInvalid` | `boolean` | Whether the checkbox group is in an invalid state |
|
||||
| `isRequired` | `boolean` | Whether the checkbox group is required |
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Checkbox
|
||||
description: Checkboxes allow users to select multiple items from a list of individual items, or to mark one individual item as selected.
|
||||
icon: new
|
||||
links:
|
||||
rac: Checkbox
|
||||
source: checkbox/checkbox.tsx
|
||||
styles: checkbox.css
|
||||
storybook: Components/Forms/Checkbox
|
||||
figma: true
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { Checkbox, Label } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-basic"
|
||||
/>
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the Checkbox component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import { Checkbox, Label, Description } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Checkbox name="terms">
|
||||
<Checkbox.Control>
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox.Control>
|
||||
<Checkbox.Content>
|
||||
<Label />
|
||||
<Description /> {/* Optional */}
|
||||
</Checkbox.Content>
|
||||
</Checkbox>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-disabled"
|
||||
/>
|
||||
|
||||
### Default Selected
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-default-selected"
|
||||
/>
|
||||
|
||||
### Controlled
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-controlled"
|
||||
/>
|
||||
|
||||
### Indeterminate
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-indeterminate"
|
||||
/>
|
||||
|
||||
### With Label
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-with-label"
|
||||
/>
|
||||
|
||||
### With Description
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-with-description"
|
||||
/>
|
||||
|
||||
### Render Props
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-render-props"
|
||||
/>
|
||||
|
||||
### Form Integration
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-form"
|
||||
/>
|
||||
|
||||
### Custom Styles
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-custom-styles"
|
||||
/>
|
||||
|
||||
### Invalid
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-invalid"
|
||||
/>
|
||||
|
||||
### Custom Indicator
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-custom-indicator"
|
||||
/>
|
||||
|
||||
### Full Rounded
|
||||
|
||||
<ComponentPreview
|
||||
name="checkbox-full-rounded"
|
||||
/>
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
You can customize individual Checkbox components:
|
||||
|
||||
```tsx
|
||||
import { Checkbox, Label } from '@heroui/react';
|
||||
|
||||
function CustomCheckbox() {
|
||||
return (
|
||||
<Checkbox name="custom">
|
||||
<Checkbox.Control className="border-2 border-purple-500 data-[selected=true]:bg-purple-500">
|
||||
<Checkbox.Indicator className="text-white" />
|
||||
</Checkbox.Control>
|
||||
<Checkbox.Content>
|
||||
<Label>Custom Checkbox</Label>
|
||||
</Checkbox.Content>
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the Checkbox component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.checkbox {
|
||||
@apply inline-flex gap-3 items-center;
|
||||
}
|
||||
|
||||
.checkbox__control {
|
||||
@apply size-5 border-2 border-gray-400 rounded data-[selected=true]:bg-blue-500 data-[selected=true]:border-blue-500;
|
||||
|
||||
/* Animated background indicator */
|
||||
&::before {
|
||||
@apply bg-accent pointer-events-none absolute inset-0 z-0 origin-center scale-50 rounded-md opacity-0 content-[''];
|
||||
|
||||
transition:
|
||||
scale 200ms linear,
|
||||
opacity 200ms linear,
|
||||
background-color 200ms ease-out;
|
||||
}
|
||||
|
||||
/* Show indicator when selected */
|
||||
&[data-selected="true"]::before {
|
||||
@apply scale-100 opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox__indicator {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.checkbox__content {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The Checkbox component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/checkbox.css)):
|
||||
|
||||
- `.checkbox` - Base checkbox container
|
||||
- `.checkbox__control` - Checkbox control box
|
||||
- `.checkbox__indicator` - Checkbox checkmark indicator
|
||||
- `.checkbox__content` - Optional content container
|
||||
|
||||
### Interactive States
|
||||
|
||||
The checkbox supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
- **Selected**: `[data-selected="true"]` or `[aria-checked="true"]` (shows checkmark and background color change)
|
||||
- **Indeterminate**: `[data-indeterminate="true"]` (shows indeterminate state with dash)
|
||||
- **Invalid**: `[data-invalid="true"]` or `[aria-invalid="true"]` (shows error state with danger colors)
|
||||
- **Hover**: `:hover` or `[data-hovered="true"]`
|
||||
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]` (shows focus ring)
|
||||
- **Disabled**: `:disabled` or `[aria-disabled="true"]` (reduced opacity, no pointer events)
|
||||
- **Pressed**: `:active` or `[data-pressed="true"]`
|
||||
|
||||
## API Reference
|
||||
|
||||
### Checkbox Props
|
||||
|
||||
Inherits from [React Aria Checkbox](https://react-spectrum.adobe.com/react-aria/Checkbox.html).
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `isSelected` | `boolean` | `false` | Whether the checkbox is checked |
|
||||
| `defaultSelected` | `boolean` | `false` | Whether the checkbox is checked by default (uncontrolled) |
|
||||
| `isIndeterminate` | `boolean` | `false` | Whether the checkbox is in an indeterminate state |
|
||||
| `isDisabled` | `boolean` | `false` | Whether the checkbox is disabled |
|
||||
| `isInvalid` | `boolean` | `false` | Whether the checkbox is invalid |
|
||||
| `isReadOnly` | `boolean` | `false` | Whether the checkbox is read only |
|
||||
| `isOnSurface` | `boolean` | `false` | Whether the checkbox is displayed on a surface (affects styling) |
|
||||
| `name` | `string` | - | The name of the input element, used when submitting an HTML form |
|
||||
| `value` | `string` | - | The value of the input element, used when submitting an HTML form |
|
||||
| `onChange` | `(isSelected: boolean) => void` | - | Handler called when the checkbox value changes |
|
||||
| `children` | `React.ReactNode \| (values: CheckboxRenderProps) => React.ReactNode` | - | Checkbox content or render prop |
|
||||
|
||||
### CheckboxRenderProps
|
||||
|
||||
When using the render prop pattern, these values are provided:
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `isSelected` | `boolean` | Whether the checkbox is currently checked |
|
||||
| `isIndeterminate` | `boolean` | Whether the checkbox is in an indeterminate state |
|
||||
| `isHovered` | `boolean` | Whether the checkbox is hovered |
|
||||
| `isPressed` | `boolean` | Whether the checkbox is currently pressed |
|
||||
| `isFocused` | `boolean` | Whether the checkbox is focused |
|
||||
| `isFocusVisible` | `boolean` | Whether the checkbox is keyboard focused |
|
||||
| `isDisabled` | `boolean` | Whether the checkbox is disabled |
|
||||
| `isReadOnly` | `boolean` | Whether the checkbox is read only |
|
||||
@@ -4,7 +4,7 @@ description: Small informational badges for displaying labels, statuses, and cat
|
||||
links:
|
||||
source: chip/chip.tsx
|
||||
styles: chip.css
|
||||
storybook: chip
|
||||
storybook: Components/DataDisplay/Chip
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -80,29 +80,41 @@ The Chip component uses these CSS classes ([View source styles](https://github.c
|
||||
#### Base Classes
|
||||
- `.chip` - Base chip styles
|
||||
|
||||
#### Type Classes
|
||||
- `.chip--accent` - Accent type variant
|
||||
- `.chip--danger` - Danger type variant
|
||||
- `.chip--default` - Default type variant
|
||||
- `.chip--success` - Success type variant
|
||||
- `.chip--warning` - Warning type variant
|
||||
#### Color Classes
|
||||
- `.chip--accent` - Accent color variant
|
||||
- `.chip--danger` - Danger color variant
|
||||
- `.chip--default` - Default color variant
|
||||
- `.chip--success` - Success color variant
|
||||
- `.chip--warning` - Warning color variant
|
||||
|
||||
#### Variant Classes
|
||||
- `.chip--primary` - Primary variant with filled background
|
||||
- `.chip--secondary` - Secondary variant with border
|
||||
- `.chip--tertiary` - Tertiary variant with transparent background
|
||||
- `.chip--soft` - Soft variant with lighter background
|
||||
|
||||
#### Size Classes
|
||||
- `.chip--sm` - Small size
|
||||
- `.chip--md` - Medium size (default)
|
||||
- `.chip--lg` - Large size
|
||||
|
||||
#### Compound Variant Classes
|
||||
- `.chip--primary.chip--accent` - Primary accent combination
|
||||
- `.chip--primary.chip--success` - Primary success combination
|
||||
- `.chip--primary.chip--warning` - Primary warning combination
|
||||
- `.chip--primary.chip--danger` - Primary danger combination
|
||||
|
||||
### Interactive States
|
||||
Chips support combining variant and color classes (e.g., `.chip--secondary.chip--accent`). The following combinations have default styles defined:
|
||||
|
||||
The component supports focus visibility:
|
||||
**Primary Variants:**
|
||||
- `.chip--primary.chip--accent` - Primary accent combination with filled background
|
||||
- `.chip--primary.chip--success` - Primary success combination with filled background
|
||||
- `.chip--primary.chip--warning` - Primary warning combination with filled background
|
||||
- `.chip--primary.chip--danger` - Primary danger combination with filled background
|
||||
|
||||
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]`
|
||||
**Soft Variants:**
|
||||
- `.chip--accent.chip--soft` - Soft accent combination with lighter background
|
||||
- `.chip--success.chip--soft` - Soft success combination with lighter background
|
||||
- `.chip--warning.chip--soft` - Soft warning combination with lighter background
|
||||
- `.chip--danger.chip--soft` - Soft danger combination with lighter background
|
||||
|
||||
**Note:** You can apply custom styles to any variant-color combination (e.g., `.chip--secondary.chip--accent`, `.chip--tertiary.chip--success`) using the `@layer components` directive in your CSS.
|
||||
|
||||
## API Reference
|
||||
|
||||
@@ -112,5 +124,6 @@ The component supports focus visibility:
|
||||
|------|------|---------|-------------|
|
||||
| `children` | `React.ReactNode` | - | Content to display inside the chip |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `type` | `"default" \| "accent" \| "success" \| "warning" \| "danger"` | `"default"` | Type variant of the chip |
|
||||
| `variant` | `"primary" \| "secondary" \| "tertiary"` | `"secondary"` | Visual style variant |
|
||||
| `color` | `"default" \| "accent" \| "success" \| "warning" \| "danger"` | `"default"` | Color variant of the chip |
|
||||
| `variant` | `"primary" \| "secondary" \| "tertiary" \| "soft"` | `"secondary"` | Visual style variant |
|
||||
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size of the chip |
|
||||
|
||||
@@ -5,7 +5,7 @@ links:
|
||||
rac: Button
|
||||
source: close-button/close-button.tsx
|
||||
styles: close-button.css
|
||||
storybook: close-button
|
||||
storybook: Components/Buttons/CloseButton
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
@@ -5,7 +5,6 @@ links:
|
||||
rac: TextField
|
||||
source: description/description.tsx
|
||||
styles: description.css
|
||||
storybook: description
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: DisclosureGroup
|
||||
description: Container that manages multiple Disclosure items with coordinated expanded states
|
||||
icon: updated
|
||||
links:
|
||||
rac: DisclosureGroup
|
||||
source: disclosure-group/disclosure-group.tsx
|
||||
styles: disclosure-group.css
|
||||
storybook: disclosure-group
|
||||
storybook: Components/Navigation/DisclosureGroup
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -31,14 +30,14 @@ import {DisclosureGroup, Disclosure} from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<DisclosureGroup>
|
||||
<Disclosure.Root id="item1">
|
||||
<Disclosure id="item1">
|
||||
<Disclosure.Heading>
|
||||
<Disclosure.Trigger>
|
||||
<Disclosure.Indicator />
|
||||
</Disclosure.Trigger>
|
||||
</Disclosure.Heading>
|
||||
<Disclosure.Content />
|
||||
</Disclosure.Root>
|
||||
</Disclosure>
|
||||
</DisclosureGroup>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Disclosure
|
||||
description: A disclosure is a collapsible section with a header containing a heading and a trigger button, and a panel that wraps the content.
|
||||
icon: updated
|
||||
links:
|
||||
rac: Disclosure
|
||||
source: disclosure/disclosure.tsx
|
||||
styles: disclosure.css
|
||||
storybook: disclosure
|
||||
storybook: Components/Navigation/Disclosure
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -30,14 +29,14 @@ Import the Disclosure component and access all parts using dot notation.
|
||||
import { Disclosure } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Disclosure.Root>
|
||||
<Disclosure>
|
||||
<Disclosure.Heading>
|
||||
<Disclosure.Trigger>
|
||||
<Disclosure.Indicator />
|
||||
</Disclosure.Trigger>
|
||||
</Disclosure.Heading>
|
||||
<Disclosure.Content/>
|
||||
</Disclosure.Root>
|
||||
</Disclosure>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -52,7 +51,7 @@ import { Disclosure } from '@heroui/react';
|
||||
|
||||
function CustomDisclosure() {
|
||||
return (
|
||||
<Disclosure.Root className="border rounded-lg p-4">
|
||||
<Disclosure className="border rounded-lg p-4">
|
||||
<Disclosure.Heading>
|
||||
<Disclosure.Trigger className="text-lg font-semibold">
|
||||
Click to expand
|
||||
@@ -64,7 +63,7 @@ function CustomDisclosure() {
|
||||
Hidden content
|
||||
</Disclosure.Body>
|
||||
</Disclosure.Content>
|
||||
</Disclosure.Root>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -119,7 +118,7 @@ The component supports both CSS pseudo-classes and data attributes for flexibili
|
||||
|
||||
## API Reference
|
||||
|
||||
### Disclosure.Root Props
|
||||
### Disclosure Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
|
||||
@@ -5,7 +5,6 @@ links:
|
||||
rac: FieldError
|
||||
source: field-error/field-error.tsx
|
||||
styles: field-error.css
|
||||
storybook: field-error
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: updated
|
||||
links:
|
||||
source: fieldset/fieldset.tsx
|
||||
styles: fieldset.css
|
||||
storybook: fieldset
|
||||
storybook: Components/Forms/Fieldset
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -18,6 +18,12 @@ import { Fieldset } from '@heroui/react';
|
||||
|
||||
<ComponentPreview name="fieldset-basic" />
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, form controls within Fieldset automatically apply on-surface styling.
|
||||
|
||||
<ComponentPreview name="fieldset-on-surface" />
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the Fieldset component and access all parts using dot notation.
|
||||
@@ -26,7 +32,7 @@ Import the Fieldset component and access all parts using dot notation.
|
||||
import { Fieldset } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Fieldset.Root>
|
||||
<Fieldset>
|
||||
<Fieldset.Legend />
|
||||
<Fieldset.Group>
|
||||
{/* form fields go here */}
|
||||
@@ -34,7 +40,7 @@ export default () => (
|
||||
<Fieldset.Actions>
|
||||
{/* action buttons go here */}
|
||||
</Fieldset.Actions>
|
||||
</Fieldset.Root>
|
||||
</Fieldset>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -51,7 +57,7 @@ import { Fieldset, TextField, Label, Input } from '@heroui/react';
|
||||
|
||||
function CustomFieldset() {
|
||||
return (
|
||||
<Fieldset.Root className="rounded-xl border border-border bg-surface-2 p-6 shadow-sm">
|
||||
<Fieldset className="rounded-xl border border-border bg-surface p-6 shadow-sm">
|
||||
<Fieldset.Legend className="text-lg font-semibold">Team members</Fieldset.Legend>
|
||||
<Fieldset.Group className="grid gap-4 md:grid-cols-2">
|
||||
<TextField>
|
||||
@@ -66,7 +72,7 @@ function CustomFieldset() {
|
||||
<Fieldset.Actions className="justify-end gap-3">
|
||||
{/* Action buttons */}
|
||||
</Fieldset.Actions>
|
||||
</Fieldset.Root>
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -78,7 +84,7 @@ Use the `@layer components` directive to target Fieldset BEM-style classes.
|
||||
```css
|
||||
@layer components {
|
||||
.fieldset {
|
||||
@apply gap-5 rounded-xl border border-border/60 bg-surface-2 p-6 shadow-field;
|
||||
@apply gap-5 rounded-xl border border-border/60 bg-surface p-6 shadow-field;
|
||||
}
|
||||
|
||||
.fieldset__legend {
|
||||
@@ -106,7 +112,7 @@ The Fieldset compound component exposes these CSS selectors:
|
||||
|
||||
## API Reference
|
||||
|
||||
### Fieldset.Root Props
|
||||
### Fieldset Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
|
||||
@@ -45,7 +45,7 @@ import {Form, TextField, Label, Input, FieldError, Button} from '@heroui/react';
|
||||
|
||||
function CustomForm() {
|
||||
return (
|
||||
<Form className="w-full max-w-md space-y-4 rounded-lg border border-border bg-surface-2 p-6">
|
||||
<Form className="w-full max-w-md space-y-4 rounded-lg border border-border bg-surface p-6">
|
||||
<TextField>
|
||||
<Label className="text-sm font-medium">Email</Label>
|
||||
<Input className="rounded-full border-border/60" placeholder="Enter your email" />
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
---
|
||||
title: InputOTP
|
||||
description: A one-time password input component for verification codes and secure authentication
|
||||
icon: new
|
||||
links:
|
||||
source: input-otp/input-otp.tsx
|
||||
styles: input-otp.css
|
||||
storybook: Components/Forms/InputOTP
|
||||
figma: true
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { InputOTP } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview name="input-otp-basic" />
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the InputOTP component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import { InputOTP } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<InputOTP maxLength={6}>
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={0} />
|
||||
<InputOTP.Slot index={1} />
|
||||
{/* ...rest of the slots */}
|
||||
</InputOTP.Group>
|
||||
<InputOTP.Separator />
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={3} />
|
||||
{/* ...rest of the slots */}
|
||||
</InputOTP.Group>
|
||||
</InputOTP>
|
||||
)
|
||||
```
|
||||
|
||||
> **InputOTP** is built on top of [input-otp](https://github.com/guilhermerodz/input-otp) by [@guilherme_rodz](https://twitter.com/guilherme_rodz), providing a flexible and accessible foundation for OTP input components.
|
||||
|
||||
### Four Digits
|
||||
|
||||
<ComponentPreview name="input-otp-four-digits" />
|
||||
|
||||
### Disabled State
|
||||
|
||||
<ComponentPreview name="input-otp-disabled" />
|
||||
|
||||
### With Pattern
|
||||
|
||||
Use the `pattern` prop to restrict input to specific characters. HeroUI exports common patterns like `REGEXP_ONLY_CHARS` and `REGEXP_ONLY_DIGITS`.
|
||||
|
||||
<ComponentPreview name="input-otp-with-pattern" />
|
||||
|
||||
### Controlled
|
||||
|
||||
Control the value to synchronize with state, clear the input, or implement custom validation.
|
||||
|
||||
<ComponentPreview name="input-otp-controlled" />
|
||||
|
||||
### With Validation
|
||||
|
||||
Use `isInvalid` together with validation messages to surface errors.
|
||||
|
||||
<ComponentPreview name="input-otp-with-validation" />
|
||||
|
||||
### On Complete
|
||||
|
||||
Use the `onComplete` callback to trigger actions when all slots are filled.
|
||||
|
||||
<ComponentPreview name="input-otp-on-complete" />
|
||||
|
||||
### Form Example
|
||||
|
||||
A complete two-factor authentication form with validation and submission.
|
||||
|
||||
<ComponentPreview name="input-otp-form-example" />
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, InputOTP automatically applies on-surface styling.
|
||||
|
||||
<ComponentPreview name="input-otp-on-surface" />
|
||||
|
||||
<RelatedComponents component="inputotp" />
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import {InputOTP, Label} from '@heroui/react';
|
||||
|
||||
function CustomInputOTP() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm font-semibold">Enter verification code</Label>
|
||||
<InputOTP
|
||||
className="gap-3"
|
||||
containerClassName="gap-4"
|
||||
maxLength={6}
|
||||
>
|
||||
<InputOTP.Group className="gap-3">
|
||||
<InputOTP.Slot
|
||||
className="size-12 rounded-lg border-2 text-lg font-bold"
|
||||
index={0}
|
||||
/>
|
||||
<InputOTP.Slot
|
||||
className="size-12 rounded-lg border-2 text-lg font-bold"
|
||||
index={1}
|
||||
/>
|
||||
<InputOTP.Slot
|
||||
className="size-12 rounded-lg border-2 text-lg font-bold"
|
||||
index={2}
|
||||
/>
|
||||
</InputOTP.Group>
|
||||
<InputOTP.Separator className="bg-border h-1 w-2 rounded-full" />
|
||||
<InputOTP.Group className="gap-3">
|
||||
<InputOTP.Slot
|
||||
className="size-12 rounded-lg border-2 text-lg font-bold"
|
||||
index={3}
|
||||
/>
|
||||
<InputOTP.Slot
|
||||
className="size-12 rounded-lg border-2 text-lg font-bold"
|
||||
index={4}
|
||||
/>
|
||||
<InputOTP.Slot
|
||||
className="size-12 rounded-lg border-2 text-lg font-bold"
|
||||
index={5}
|
||||
/>
|
||||
</InputOTP.Group>
|
||||
</InputOTP>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the InputOTP component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.input-otp {
|
||||
@apply gap-3;
|
||||
}
|
||||
|
||||
.input-otp__slot {
|
||||
@apply size-12 rounded-xl border-2 font-bold;
|
||||
}
|
||||
|
||||
.input-otp__slot[data-active="true"] {
|
||||
@apply border-primary-500 ring-2 ring-primary-200;
|
||||
}
|
||||
|
||||
.input-otp__separator {
|
||||
@apply w-2 h-1 bg-border-strong rounded-full;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The InputOTP component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/input-otp.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.input-otp` - Base container
|
||||
- `.input-otp__container` - Inner container from input-otp library
|
||||
- `.input-otp__group` - Group of slots
|
||||
- `.input-otp__slot` - Individual input slot
|
||||
- `.input-otp__slot-value` - The character inside a slot
|
||||
- `.input-otp__caret` - Blinking caret indicator
|
||||
- `.input-otp__separator` - Visual separator between groups
|
||||
|
||||
#### State Classes
|
||||
- `.input-otp__slot[data-active="true"]` - Currently active slot
|
||||
- `.input-otp__slot[data-filled="true"]` - Slot with a character
|
||||
- `.input-otp__slot[data-disabled="true"]` - Disabled slot
|
||||
- `.input-otp__slot[data-invalid="true"]` - Invalid slot
|
||||
- `.input-otp__container[data-disabled="true"]` - Disabled container
|
||||
|
||||
### Interactive States
|
||||
|
||||
The component supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
- **Hover**: `:hover` or `[data-hovered="true"]` on slot
|
||||
- **Active**: `[data-active="true"]` on slot (currently focused)
|
||||
- **Filled**: `[data-filled="true"]` on slot (contains a character)
|
||||
- **Disabled**: `[data-disabled="true"]` on container and slots
|
||||
- **Invalid**: `[data-invalid="true"]` on slots
|
||||
|
||||
## API Reference
|
||||
|
||||
### InputOTP Props
|
||||
|
||||
InputOTP is built on top of the [input-otp](https://github.com/guilhermerodz/input-otp) library with additional features.
|
||||
|
||||
#### Base Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `maxLength` | `number` | - | **Required.** Number of input slots. |
|
||||
| `value` | `string` | - | Controlled value (uncontrolled if not provided). |
|
||||
| `onChange` | `(value: string) => void` | - | Handler called when the value changes. |
|
||||
| `onComplete` | `(value: string) => void` | - | Handler called when all slots are filled. |
|
||||
| `className` | `string` | - | Additional CSS classes for the container. |
|
||||
| `containerClassName` | `string` | - | CSS classes for the inner container. |
|
||||
| `isOnSurface` | `boolean` | `false` | Whether the input is displayed on a surface (affects styling) |
|
||||
| `children` | `React.ReactNode` | - | InputOTP.Group, InputOTP.Slot, and InputOTP.Separator components. |
|
||||
|
||||
#### Validation Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `isDisabled` | `boolean` | `false` | Whether the input is disabled. |
|
||||
| `isInvalid` | `boolean` | `false` | Whether the input is in an invalid state. |
|
||||
| `validationErrors` | `string[]` | - | Server-side or custom validation errors. |
|
||||
| `validationDetails` | `ValidityState` | - | HTML5 validation details. |
|
||||
|
||||
#### Input Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `pattern` | `string` | - | Regex pattern for allowed characters (e.g., `REGEXP_ONLY_DIGITS`). |
|
||||
| `textAlign` | `'left' \| 'center' \| 'right'` | `'left'` | Text alignment within slots. |
|
||||
| `inputMode` | `'numeric' \| 'text' \| 'decimal' \| 'tel' \| 'search' \| 'email' \| 'url'` | `'numeric'` | Virtual keyboard type on mobile devices. |
|
||||
| `placeholder` | `string` | - | Placeholder text for empty slots. |
|
||||
| `pasteTransformer` | `(text: string) => string` | - | Transform pasted text (e.g., remove hyphens). |
|
||||
|
||||
#### Form Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `name` | `string` | - | Name attribute for form submission. |
|
||||
| `autoFocus` | `boolean` | - | Whether to focus the first slot on mount. |
|
||||
|
||||
### InputOTP.Group Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes for the group. |
|
||||
| `children` | `React.ReactNode` | - | InputOTP.Slot components. |
|
||||
|
||||
### InputOTP.Slot Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `index` | `number` | - | **Required.** Zero-based index of the slot. |
|
||||
| `className` | `string` | - | Additional CSS classes for the slot. |
|
||||
|
||||
### InputOTP.Separator Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes for the separator. |
|
||||
|
||||
### Exported Patterns
|
||||
|
||||
HeroUI re-exports common regex patterns from input-otp for convenience:
|
||||
|
||||
```tsx
|
||||
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_CHARS, REGEXP_ONLY_DIGITS_AND_CHARS } from '@heroui/react';
|
||||
|
||||
// Use with pattern prop
|
||||
<InputOTP pattern={REGEXP_ONLY_DIGITS} maxLength={6}>
|
||||
{/* ... */}
|
||||
</InputOTP>
|
||||
```
|
||||
|
||||
- **REGEXP_ONLY_DIGITS** - Only numeric characters (0-9)
|
||||
- **REGEXP_ONLY_CHARS** - Only alphabetic characters (a-z, A-Z)
|
||||
- **REGEXP_ONLY_DIGITS_AND_CHARS** - Alphanumeric characters (0-9, a-z, A-Z)
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: Input
|
||||
description: Primitive single-line text input component that accepts standard HTML attributes
|
||||
icon: updated
|
||||
links:
|
||||
rac: Input
|
||||
source: input/input.tsx
|
||||
styles: input.css
|
||||
storybook: input
|
||||
storybook: Components/Forms/Input
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -30,6 +31,12 @@ import { Input } from '@heroui/react';
|
||||
|
||||
<ComponentPreview name="input-controlled" />
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, Input automatically applies on-surface styling.
|
||||
|
||||
<ComponentPreview name="input-on-surface" />
|
||||
|
||||
<RelatedComponents component="input" />
|
||||
|
||||
## Styling
|
||||
@@ -45,7 +52,7 @@ function CustomInput() {
|
||||
<Label htmlFor="custom-input">Project name</Label>
|
||||
<Input
|
||||
id="custom-input"
|
||||
className="rounded-xl border border-border/70 bg-surface-2 px-4 py-2 text-sm shadow-sm focus-visible:border-primary"
|
||||
className="rounded-xl border border-border/70 bgsurface px-4 py-2 text-sm shadow-sm focus-visible:border-primary"
|
||||
placeholder="New web app"
|
||||
/>
|
||||
</div>
|
||||
@@ -60,11 +67,11 @@ The base class `.input` powers every instance. Override it once with `@layer com
|
||||
```css
|
||||
@layer components {
|
||||
.input {
|
||||
@apply rounded-lg border border-border bg-surface-2 px-4 py-2 text-sm shadow-sm transition-colors;
|
||||
@apply rounded-lg border border-border bgsurface px-4 py-2 text-sm shadow-sm transition-colors;
|
||||
|
||||
&:hover,
|
||||
&[data-hovered="true"] {
|
||||
@apply bg-surface-3 border-border/80;
|
||||
@apply bg-surface-secondary border-border/80;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
@@ -116,5 +123,6 @@ Input accepts all standard HTML `<input>` attributes plus the following:
|
||||
| `min` | `number \| string` | - | Minimum value (for number/date inputs). |
|
||||
| `max` | `number \| string` | - | Maximum value (for number/date inputs). |
|
||||
| `step` | `number \| string` | - | Stepping interval (for number inputs). |
|
||||
| `isOnSurface` | `boolean` | `false` | Whether the input is displayed on a surface (affects styling) |
|
||||
|
||||
> For validation props like `isInvalid`, `isRequired`, and error handling, use **[TextField](/docs/components/text-field)** with Input as a child component.
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
---
|
||||
title: Kbd
|
||||
description: Display keyboard shortcuts and key combinations
|
||||
icon: updated
|
||||
links:
|
||||
source: kbd/kbd.tsx
|
||||
styles: kbd.css
|
||||
storybook: kbd
|
||||
storybook: Components/Typography/Kbd
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -29,10 +28,10 @@ Import the Kbd component and access all parts using dot notation.
|
||||
import { Kbd } from "@heroui/react";
|
||||
|
||||
export default () => (
|
||||
<Kbd.Root>
|
||||
<Kbd>
|
||||
<Kbd.Abbr title="Command">⌘</Kbd.Abbr>
|
||||
<Kbd.Content>K</Kbd.Content>
|
||||
</Kbd.Root>
|
||||
</Kbd>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -60,6 +59,12 @@ export default () => (
|
||||
name="kbd-special-keys"
|
||||
/>
|
||||
|
||||
### Variants
|
||||
|
||||
<ComponentPreview
|
||||
name="kbd-variants"
|
||||
/>
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
@@ -69,9 +74,9 @@ import { Kbd } from "@heroui/react";
|
||||
|
||||
function CustomKbd() {
|
||||
return (
|
||||
<Kbd.Root className="bg-gray-100 dark:bg-gray-800">
|
||||
<Kbd className="bg-gray-100 dark:bg-gray-800">
|
||||
<Kbd.Content>K</Kbd.Content>
|
||||
</Kbd.Root>
|
||||
</Kbd>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -111,11 +116,12 @@ The Kbd component uses these CSS classes ([View source styles](https://github.co
|
||||
|
||||
## API Reference
|
||||
|
||||
### Kbd.Root Props
|
||||
### Kbd Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ----------- | ----------------- | ------- | ---------------------- |
|
||||
| `children` | `React.ReactNode` | - | Content of the key |
|
||||
| `variant` | `"default" | "light"` | `default` | Variant of the keyboard key |
|
||||
| `className` | `string` | - | Custom CSS classes |
|
||||
|
||||
### Kbd.Abbr Props
|
||||
|
||||
@@ -5,7 +5,6 @@ links:
|
||||
rac: TextField
|
||||
source: label/label.tsx
|
||||
styles: label.css
|
||||
storybook: label
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Link
|
||||
description: A styled anchor component for navigation with built-in icon support
|
||||
icon: updated
|
||||
links:
|
||||
rac: Link
|
||||
source: link/link.tsx
|
||||
styles: link.css
|
||||
storybook: link
|
||||
storybook: Components/Navigation/Link
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -30,10 +29,10 @@ Import the Link component and access all parts using dot notation.
|
||||
import { Link } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Link.Root href="#">
|
||||
<Link href="#">
|
||||
Call to action
|
||||
<Link.Icon />
|
||||
</Link.Root>
|
||||
</Link>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -49,6 +48,81 @@ export default () => (
|
||||
name="link-icon-placement"
|
||||
/>
|
||||
|
||||
### Underline Variants
|
||||
|
||||
Control the underline behavior with the `underline` prop:
|
||||
|
||||
<ComponentPreview
|
||||
name="link-underline-variants"
|
||||
/>
|
||||
|
||||
- `underline="hover"` (default) - Animated underline appears on hover
|
||||
- `underline="always"` - Underline always visible (50% opacity, 100% on hover)
|
||||
- `underline="none"` - No underline
|
||||
|
||||
### Underline Offset
|
||||
|
||||
Adjust the spacing between text and underline with the `underlineOffset` prop:
|
||||
|
||||
<ComponentPreview
|
||||
name="link-underline-offset"
|
||||
/>
|
||||
|
||||
- `underlineOffset={1}` (default) - No space
|
||||
- `underlineOffset={2}` - 2px spacing
|
||||
- `underlineOffset={3}` - 4px spacing
|
||||
|
||||
### Using with Routing Libraries
|
||||
|
||||
Use the `asChild` prop to compose Link with framework-specific links like Next.js:
|
||||
|
||||
```tsx
|
||||
import { Link } from '@heroui/react';
|
||||
import NextLink from 'next/link';
|
||||
|
||||
export default function Demo() {
|
||||
return (
|
||||
<Link asChild underline="hover">
|
||||
<NextLink href="/about">
|
||||
About Page
|
||||
<Link.Icon />
|
||||
</NextLink>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Class Application
|
||||
|
||||
Since HeroUI uses BEM classes, you can apply Link styles directly to any link element without using `asChild`:
|
||||
|
||||
```tsx
|
||||
import NextLink from 'next/link';
|
||||
|
||||
// Apply classes directly
|
||||
export default function Demo() {
|
||||
return (
|
||||
<NextLink href="/about" className="link link--underline-hover link--offset-1">
|
||||
About Page
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
// Or with a native anchor
|
||||
export default function NativeLink() {
|
||||
return (
|
||||
<a href="/about" className="link link--underline-always link--offset-2">
|
||||
About Page
|
||||
</a>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Available BEM classes:
|
||||
- Base: `link`
|
||||
- Underline: `link--underline-none`, `link--underline-hover`, `link--underline-always`
|
||||
- Offset: `link--offset-1`, `link--offset-2`, `link--offset-3`
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
@@ -58,12 +132,12 @@ import { Link } from '@heroui/react';
|
||||
|
||||
function CustomLink() {
|
||||
return (
|
||||
<Link.Root
|
||||
<Link
|
||||
href="#"
|
||||
className="text-lg font-bold text-accent hover:text-accent/80"
|
||||
>
|
||||
Custom styled link
|
||||
</Link.Root>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -88,7 +162,17 @@ HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component va
|
||||
The Link component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/link.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.link` - Base link styles with underline on hover
|
||||
- `.link` - Base link styles
|
||||
|
||||
#### Underline Variants
|
||||
- `.link--underline-none` - No underline
|
||||
- `.link--underline-hover` - Animated underline on hover (default)
|
||||
- `.link--underline-always` - Always visible underline
|
||||
|
||||
#### Underline Offset
|
||||
- `.link--offset-1` - No spacing (default)
|
||||
- `.link--offset-2` - 2px spacing
|
||||
- `.link--offset-3` - 4px spacing
|
||||
|
||||
### Interactive States
|
||||
|
||||
@@ -99,7 +183,7 @@ The component supports both CSS pseudo-classes and data attributes for flexibili
|
||||
|
||||
## API Reference
|
||||
|
||||
### Link.Root Props
|
||||
### Link Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
@@ -107,6 +191,9 @@ The component supports both CSS pseudo-classes and data attributes for flexibili
|
||||
| `target` | `string` | `"_self"` | Controls where to open the linked document |
|
||||
| `rel` | `string` | - | Relationship between the current and linked documents |
|
||||
| `download` | `boolean \| string` | - | Prompts file download instead of navigation |
|
||||
| `underline` | `"none" \| "hover" \| "always"` | `"hover"` | Controls underline visibility and behavior |
|
||||
| `underlineOffset` | `1 \| 2 \| 3` | `1` | Spacing between text and underline |
|
||||
| `asChild` | `boolean` | `false` | Merge props with child element (useful for routing libraries) |
|
||||
| `isDisabled` | `boolean` | `false` | Disables pointer and keyboard interaction |
|
||||
| `className` | `string` | - | Custom classes merged with the default styles |
|
||||
| `children` | `React.ReactNode` | - | Content rendered inside the link |
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: ListBox
|
||||
description: A listbox displays a list of options and allows a user to select one or more of them
|
||||
icon: new
|
||||
links:
|
||||
rac: ListBox
|
||||
source: listbox/listbox.tsx
|
||||
styles: listbox.css
|
||||
storybook: Components/Collections/ListBox
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { ListBox } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-default"
|
||||
/>
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the ListBox component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import { ListBox, Label, Description, Header } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<ListBox>
|
||||
<ListBox.Item>
|
||||
<Label />
|
||||
<Description />
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Section>
|
||||
<Header />
|
||||
<ListBox.Item>
|
||||
<Label />
|
||||
</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
</ListBox>
|
||||
)
|
||||
```
|
||||
|
||||
### With Sections
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-with-sections"
|
||||
/>
|
||||
|
||||
### Multi Select
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-multi-select"
|
||||
/>
|
||||
|
||||
### With Disabled Items
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-with-disabled-items"
|
||||
/>
|
||||
|
||||
### Custom Check Icon
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-custom-check-icon"
|
||||
/>
|
||||
|
||||
### Controlled
|
||||
|
||||
<ComponentPreview
|
||||
name="listbox-controlled"
|
||||
/>
|
||||
|
||||
<RelatedComponents component="listbox" />
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import { ListBox } from '@heroui/react';
|
||||
|
||||
function CustomListBox() {
|
||||
return (
|
||||
<ListBox className="border rounded-lg p-2 bg-surface">
|
||||
<ListBox.Item id="1" textValue="Item 1" className="hover:bg-surface-secondary">
|
||||
Item 1
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the ListBox component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.listbox {
|
||||
@apply rounded-lg border border-border bg-surface p-2;
|
||||
}
|
||||
|
||||
.listbox-item {
|
||||
@apply rounded px-2 py-1 cursor-pointer;
|
||||
}
|
||||
|
||||
.listbox-item--danger {
|
||||
@apply text-danger;
|
||||
}
|
||||
|
||||
.listbox-item__indicator {
|
||||
@apply text-accent;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The ListBox component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/listbox.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.listbox` - Base listbox container
|
||||
- `.listbox-item` - Individual listbox item
|
||||
- `.listbox-item__indicator` - Selection indicator icon
|
||||
- `.listbox-section` - Section container for grouping items
|
||||
|
||||
#### Variant Classes
|
||||
- `.listbox--default` - Default variant styling
|
||||
- `.listbox--danger` - Danger variant styling
|
||||
- `.listbox-item--default` - Default item variant
|
||||
- `.listbox-item--danger` - Danger item variant
|
||||
|
||||
#### State Classes
|
||||
- `.listbox-item[data-selected="true"]` - Selected item state
|
||||
- `.listbox-item[data-focus-visible="true"]` - Focused item state
|
||||
- `.listbox-item[data-disabled="true"]` - Disabled item state
|
||||
- `.listbox-item__indicator[data-visible="true"]` - Visible indicator state
|
||||
|
||||
### Interactive States
|
||||
|
||||
The component supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
- **Hover**: `:hover` or `[data-hovered="true"]` on item
|
||||
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]` on item
|
||||
- **Selected**: `[data-selected="true"]` on item
|
||||
- **Disabled**: `:disabled` or `[data-disabled="true"]` on item
|
||||
|
||||
## API Reference
|
||||
|
||||
### ListBox Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `aria-label` | `string` | - | Accessibility label for the listbox |
|
||||
| `aria-labelledby` | `string` | - | ID of element that labels the listbox |
|
||||
| `selectionMode` | `"none" \| "single" \| "multiple"` | `"single"` | Selection behavior |
|
||||
| `selectedKeys` | `Selection` | - | Controlled selected keys |
|
||||
| `defaultSelectedKeys` | `Selection` | - | Initial selected keys |
|
||||
| `onSelectionChange` | `(keys: Selection) => void` | - | Handler called when selection changes |
|
||||
| `disabledKeys` | `Iterable<Key>` | - | Keys of disabled items |
|
||||
| `onAction` | `(key: Key) => void` | - | Handler called when an item is activated |
|
||||
| `variant` | `"default" \| "danger"` | `"default"` | Visual variant |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | ListBox items and sections |
|
||||
|
||||
### ListBox.Item Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `id` | `Key` | - | Unique identifier for the item |
|
||||
| `textValue` | `string` | - | Text value for accessibility and typeahead |
|
||||
| `isDisabled` | `boolean` | `false` | Whether this item is disabled |
|
||||
| `variant` | `"default" \| "danger"` | `"default"` | Visual variant |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Item content or render function |
|
||||
|
||||
### ListBox.ItemIndicator Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Custom indicator content or render function |
|
||||
|
||||
### ListBox.Section Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | Section content including Header and Items |
|
||||
|
||||
### RenderProps
|
||||
|
||||
When using render functions with ListBox.Item or ListBox.ItemIndicator, these values are provided:
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `isSelected` | `boolean` | Whether the item is selected |
|
||||
| `isFocused` | `boolean` | Whether the item is focused |
|
||||
| `isDisabled` | `boolean` | Whether the item is disabled |
|
||||
| `isPressed` | `boolean` | Whether the item is being pressed |
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import { ListBox, Label, Description } from '@heroui/react';
|
||||
|
||||
<ListBox aria-label="Users" selectionMode="single">
|
||||
<ListBox.Item id="1" textValue="Bob">
|
||||
<Label>Bob</Label>
|
||||
<Description>bob@heroui.com</Description>
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="2" textValue="Alice">
|
||||
<Label>Alice</Label>
|
||||
<Description>alice@heroui.com</Description>
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
```
|
||||
|
||||
### With Sections
|
||||
|
||||
```tsx
|
||||
import { ListBox, Header, Separator } from '@heroui/react';
|
||||
|
||||
<ListBox aria-label="Actions" selectionMode="none" onAction={(key) => console.log(key)}>
|
||||
<ListBox.Section>
|
||||
<Header>Actions</Header>
|
||||
<ListBox.Item id="new" textValue="New file">New file</ListBox.Item>
|
||||
<ListBox.Item id="edit" textValue="Edit file">Edit file</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
<Separator />
|
||||
<ListBox.Section>
|
||||
<Header>Danger zone</Header>
|
||||
<ListBox.Item id="delete" textValue="Delete" variant="danger">Delete</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
</ListBox>
|
||||
```
|
||||
|
||||
### Controlled Selection
|
||||
|
||||
```tsx
|
||||
import { ListBox, Selection } from '@heroui/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
function ControlledListBox() {
|
||||
const [selected, setSelected] = useState<Selection>(new Set(["1"]));
|
||||
|
||||
return (
|
||||
<ListBox
|
||||
aria-label="Options"
|
||||
selectedKeys={selected}
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={setSelected}
|
||||
>
|
||||
<ListBox.Item id="1" textValue="Option 1">Option 1</ListBox.Item>
|
||||
<ListBox.Item id="2" textValue="Option 2">Option 2</ListBox.Item>
|
||||
<ListBox.Item id="3" textValue="Option 3">Option 3</ListBox.Item>
|
||||
</ListBox>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Indicator
|
||||
|
||||
```tsx
|
||||
import { ListBox, ListBoxItemIndicator } from '@heroui/react';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
<ListBox aria-label="Options" selectionMode="multiple">
|
||||
<ListBox.Item id="1" textValue="Option 1">
|
||||
Option 1
|
||||
<ListBox.ItemIndicator>
|
||||
{({isSelected}) =>
|
||||
isSelected ? <Icon icon="gravity-ui:check" /> : null
|
||||
}
|
||||
</ListBox.ItemIndicator>
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
The ListBox component implements the ARIA listbox pattern and provides:
|
||||
|
||||
- Full keyboard navigation support
|
||||
- Screen reader announcements for selection changes
|
||||
- Proper focus management
|
||||
- Support for disabled states
|
||||
- Typeahead search functionality
|
||||
|
||||
For more information, see the [React Aria ListBox documentation](https://react-spectrum.adobe.com/react-aria/ListBox.html).
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Popover
|
||||
description: Displays rich content in a portal triggered by a button or any custom element
|
||||
icon: updated
|
||||
links:
|
||||
rac: Popover
|
||||
source: popover/popover.tsx
|
||||
styles: popover.css
|
||||
storybook: popover
|
||||
storybook: Components/Overlays/Popover
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -29,7 +28,7 @@ Import the Popover component and access all parts using dot notation.
|
||||
import { Popover } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Popover.Root>
|
||||
<Popover>
|
||||
<Popover.Trigger/>
|
||||
<Popover.Content>
|
||||
<Popover.Arrow />
|
||||
@@ -38,7 +37,7 @@ export default () => (
|
||||
{/* content goes here */}
|
||||
</Popover.Dialog>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Popover>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -69,7 +68,7 @@ import { Popover, Button } from '@heroui/react';
|
||||
|
||||
function CustomPopover() {
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover>
|
||||
<Popover.Trigger>
|
||||
<Button>Open</Button>
|
||||
</Popover.Trigger>
|
||||
@@ -79,7 +78,7 @@ function CustomPopover() {
|
||||
<p>This popover has custom styling</p>
|
||||
</Popover.Dialog>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -128,7 +127,7 @@ The component supports animation states:
|
||||
|
||||
## API Reference
|
||||
|
||||
### Popover.Root Props
|
||||
### Popover Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
|
||||
@@ -6,7 +6,7 @@ links:
|
||||
rac: RadioGroup
|
||||
source: radio-group/radio-group.tsx
|
||||
styles: radio-group.css
|
||||
storybook: radio-group
|
||||
storybook: Components/Forms/RadioGroup
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -33,7 +33,7 @@ export default () => (
|
||||
<RadioGroup>
|
||||
<Label />
|
||||
<Description />
|
||||
<Radio.Root value="option1">
|
||||
<Radio value="option1">
|
||||
<Radio.Control>
|
||||
<Radio.Indicator>
|
||||
<span>✓</span> {/* Custom indicator (optional) */}
|
||||
@@ -43,7 +43,7 @@ export default () => (
|
||||
<Label />
|
||||
<Description />
|
||||
</Radio.Content>
|
||||
</Radio.Root>
|
||||
</Radio>
|
||||
<FieldError />
|
||||
</RadioGroup>
|
||||
)
|
||||
@@ -87,6 +87,14 @@ Combine `defaultValue` with `onChange` when you only need to react to updates.
|
||||
name="radio-group-disabled"
|
||||
/>
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, RadioGroup automatically applies on-surface styling.
|
||||
|
||||
<ComponentPreview
|
||||
name="radio-group-on-surface"
|
||||
/>
|
||||
|
||||
### Delivery & Payment
|
||||
|
||||
<ComponentPreview
|
||||
@@ -107,27 +115,27 @@ import { RadioGroup, Radio } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<RadioGroup defaultValue="premium" name="plan">
|
||||
<Radio.Root
|
||||
<Radio
|
||||
className="border-border group cursor-pointer rounded-xl border-2 p-4 hover:border-blue-300 data-[selected=true]:border-blue-500 data-[selected=true]:bg-blue-500/10"
|
||||
value="basic"
|
||||
>
|
||||
<Radio.Indicator className="border-border border-2 group-hover:border-blue-400 group-data-[selected=true]:border-blue-500 group-data-[selected=true]:bg-blue-500" />
|
||||
Basic Plan
|
||||
</Radio.Root>
|
||||
<Radio.Root
|
||||
</Radio>
|
||||
<Radio
|
||||
className="border-border group cursor-pointer rounded-xl border-2 p-4 hover:border-purple-300 data-[selected=true]:border-purple-500 data-[selected=true]:bg-purple-500/10"
|
||||
value="premium"
|
||||
>
|
||||
<Radio.Indicator className="border-border border-2 group-hover:border-purple-400 group-data-[selected=true]:border-purple-500 group-data-[selected=true]:bg-purple-500" />
|
||||
Premium Plan
|
||||
</Radio.Root>
|
||||
<Radio.Root
|
||||
</Radio>
|
||||
<Radio
|
||||
className="border-border group cursor-pointer rounded-xl border-2 p-4 hover:border-emerald-300 data-[selected=true]:border-emerald-500 data-[selected=true]:bg-emerald-500/10"
|
||||
value="business"
|
||||
>
|
||||
<Radio.Indicator className="border-border border-2 group-hover:border-emerald-400 group-data-[selected=true]:border-emerald-500 group-data-[selected=true]:bg-emerald-500" />
|
||||
Business Plan
|
||||
</Radio.Root>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
);
|
||||
```
|
||||
@@ -201,11 +209,12 @@ The radio supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
| `isRequired` | `boolean` | `false` | Whether the radio group is required |
|
||||
| `isReadOnly` | `boolean` | `false` | Whether the radio group is read only |
|
||||
| `isInvalid` | `boolean` | `false` | Whether the radio group is in an invalid state |
|
||||
| `isOnSurface` | `boolean` | `false` | Whether the radio group is displayed on a surface (affects styling) |
|
||||
| `name` | `string` | - | The name of the radio group, used when submitting an HTML form |
|
||||
| `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | The orientation of the radio group |
|
||||
| `children` | `React.ReactNode \| (values: RadioGroupRenderProps) => React.ReactNode` | - | Radio group content or render prop |
|
||||
|
||||
### Radio.Root Props
|
||||
### Radio Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
---
|
||||
title: Select
|
||||
description: A select displays a collapsible list of options and allows a user to select one of them
|
||||
icon: new
|
||||
links:
|
||||
rac: Select
|
||||
source: select/select.tsx
|
||||
styles: select.css
|
||||
storybook: Components/Pickers/Select
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { Select } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="select-default"
|
||||
/>
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the Select component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import { Select, Label, Description, Header, ListBox, Separator } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Select>
|
||||
<Label />
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Description />
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
<ListBox.Item>
|
||||
<Label />
|
||||
<Description />
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Section>
|
||||
<Header />
|
||||
<ListBox.Item>
|
||||
<Label />
|
||||
</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
)
|
||||
```
|
||||
|
||||
### With Description
|
||||
|
||||
<ComponentPreview
|
||||
name="select-with-description"
|
||||
/>
|
||||
|
||||
### Multiple Select
|
||||
|
||||
<ComponentPreview
|
||||
name="select-multiple-select"
|
||||
/>
|
||||
|
||||
### With Sections
|
||||
|
||||
<ComponentPreview
|
||||
name="select-with-sections"
|
||||
/>
|
||||
|
||||
### With Disabled Options
|
||||
|
||||
<ComponentPreview
|
||||
name="select-with-disabled-options"
|
||||
/>
|
||||
|
||||
### Custom Indicator
|
||||
|
||||
<ComponentPreview
|
||||
name="select-custom-indicator"
|
||||
/>
|
||||
|
||||
### Required
|
||||
|
||||
<ComponentPreview
|
||||
name="select-required"
|
||||
/>
|
||||
|
||||
### On Surface
|
||||
|
||||
<ComponentPreview
|
||||
name="select-on-surface"
|
||||
/>
|
||||
|
||||
### Custom Value
|
||||
|
||||
<ComponentPreview
|
||||
name="select-custom-value"
|
||||
/>
|
||||
|
||||
### Controlled
|
||||
|
||||
<ComponentPreview
|
||||
name="select-controlled"
|
||||
/>
|
||||
|
||||
### Controlled Multiple
|
||||
|
||||
<ComponentPreview
|
||||
name="select-controlled-multiple"
|
||||
/>
|
||||
|
||||
### Controlled Open State
|
||||
|
||||
<ComponentPreview
|
||||
name="select-controlled-open-state"
|
||||
/>
|
||||
|
||||
### Asynchronous Loading
|
||||
|
||||
<ComponentPreview
|
||||
name="select-asynchronous-loading"
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
<ComponentPreview
|
||||
name="select-disabled"
|
||||
/>
|
||||
|
||||
<RelatedComponents component="select" />
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import { Select } from '@heroui/react';
|
||||
|
||||
function CustomSelect() {
|
||||
return (
|
||||
<Select className="w-full">
|
||||
<Label>State</Label>
|
||||
<Select.Trigger className="border rounded-lg p-2 bg-surface">
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
<ListBox.Item id="1" textValue="Item 1" className="hover:bg-surface-secondary">
|
||||
Item 1
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the Select component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.select {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
.select__trigger {
|
||||
@apply rounded-lg border border-border bg-surface p-2;
|
||||
}
|
||||
|
||||
.select__value {
|
||||
@apply text-current;
|
||||
}
|
||||
|
||||
.select__indicator {
|
||||
@apply text-muted;
|
||||
}
|
||||
|
||||
.select__content {
|
||||
@apply rounded-lg border border-border bg-surface p-2;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The Select component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/select.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.select` - Base select container
|
||||
- `.select__trigger` - The button that triggers the select
|
||||
- `.select__value` - The displayed value or placeholder
|
||||
- `.select__indicator` - The dropdown indicator icon
|
||||
- `.select__content` - The popover content container
|
||||
|
||||
#### Variant Classes
|
||||
- `.select__trigger--on-surface` - On surface variant styling
|
||||
|
||||
#### State Classes
|
||||
- `.select[data-invalid="true"]` - Invalid state
|
||||
- `.select__trigger[data-focus-visible="true"]` - Focused trigger state
|
||||
- `.select__trigger[data-disabled="true"]` - Disabled trigger state
|
||||
- `.select__value[data-placeholder="true"]` - Placeholder state
|
||||
- `.select__indicator[data-open="true"]` - Open indicator state
|
||||
|
||||
### Interactive States
|
||||
|
||||
The component supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
- **Hover**: `:hover` or `[data-hovered="true"]` on trigger
|
||||
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]` on trigger
|
||||
- **Disabled**: `:disabled` or `[data-disabled="true"]` on select
|
||||
- **Open**: `[data-open="true"]` on indicator
|
||||
|
||||
## API Reference
|
||||
|
||||
### Select Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `placeholder` | `string` | `'Select an item'` | Temporary text that occupies the select when it is empty |
|
||||
| `selectionMode` | `"single" \| "multiple"` | `"single"` | Whether single or multiple selection is enabled |
|
||||
| `isOpen` | `boolean` | - | Sets the open state of the menu (controlled) |
|
||||
| `defaultOpen` | `boolean` | - | Sets the default open state of the menu (uncontrolled) |
|
||||
| `onOpenChange` | `(isOpen: boolean) => void` | - | Handler called when the open state changes |
|
||||
| `disabledKeys` | `Iterable<Key>` | - | Keys of disabled items |
|
||||
| `isDisabled` | `boolean` | - | Whether the select is disabled |
|
||||
| `value` | `Key \| Key[] \| null` | - | Current value (controlled) |
|
||||
| `defaultValue` | `Key \| Key[] \| null` | - | Default value (uncontrolled) |
|
||||
| `onChange` | `(value: Key \| Key[] \| null) => void` | - | Handler called when the value changes |
|
||||
| `isRequired` | `boolean` | - | Whether user input is required |
|
||||
| `isInvalid` | `boolean` | - | Whether the select value is invalid |
|
||||
| `name` | `string` | - | The name of the input, used when submitting an HTML form |
|
||||
| `autoComplete` | `string` | - | Describes the type of autocomplete functionality |
|
||||
| `isOnSurface` | `boolean` | - | Whether the select is displayed on a surface component |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Select content or render function |
|
||||
|
||||
### Select.Trigger Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Trigger content or render function |
|
||||
|
||||
### Select.Value Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Value content or render function |
|
||||
|
||||
### Select.Indicator Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | Custom indicator content |
|
||||
|
||||
### Select.Content Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `placement` | `"bottom" \| "bottom left" \| "bottom right" \| "bottom start" \| "bottom end" \| "top" \| "top left" \| "top right" \| "top start" \| "top end" \| "left" \| "left top" \| "left bottom" \| "start" \| "start top" \| "start bottom" \| "right" \| "right top" \| "right bottom" \| "end" \| "end top" \| "end bottom"` | `"bottom"` | Placement of the popover relative to the trigger |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | Content children |
|
||||
|
||||
### RenderProps
|
||||
|
||||
When using render functions with Select.Value, these values are provided:
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `defaultChildren` | `ReactNode` | The default rendered value |
|
||||
| `isPlaceholder` | `boolean` | Whether the value is a placeholder |
|
||||
| `state` | `SelectState` | The state of the select |
|
||||
| `selectedItems` | `Node[]` | The currently selected items |
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import { Select, Label, ListBox } from '@heroui/react';
|
||||
|
||||
<Select className="w-[256px]" placeholder="Select one">
|
||||
<Label>State</Label>
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
<ListBox.Item id="florida" textValue="Florida">
|
||||
Florida
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="california" textValue="California">
|
||||
California
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
```
|
||||
|
||||
### With Sections
|
||||
|
||||
```tsx
|
||||
import { Select, Label, ListBox, Header, Separator } from '@heroui/react';
|
||||
|
||||
<Select className="w-[256px]" placeholder="Select a country">
|
||||
<Label>Country</Label>
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
<ListBox.Section>
|
||||
<Header>North America</Header>
|
||||
<ListBox.Item id="usa" textValue="United States">
|
||||
United States
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
<Separator />
|
||||
<ListBox.Section>
|
||||
<Header>Europe</Header>
|
||||
<ListBox.Item id="uk" textValue="United Kingdom">
|
||||
United Kingdom
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
```
|
||||
|
||||
### Controlled Selection
|
||||
|
||||
```tsx
|
||||
import type { Key } from '@heroui/react';
|
||||
|
||||
import { Select, Label, ListBox } from '@heroui/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
function ControlledSelect() {
|
||||
const [value, setValue] = useState<Key | null>('california');
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="w-[256px]"
|
||||
placeholder="Select a state"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
>
|
||||
<Label>State</Label>
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
<ListBox.Item id="california" textValue="California">
|
||||
California
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="texas" textValue="Texas">
|
||||
Texas
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Value Display
|
||||
|
||||
```tsx
|
||||
import { Select, Label, ListBox, Avatar, AvatarImage, AvatarFallback } from '@heroui/react';
|
||||
|
||||
<Select className="w-[256px]" placeholder="Select a user">
|
||||
<Label>User</Label>
|
||||
<Select.Trigger>
|
||||
<Select.Value>
|
||||
{({defaultChildren, isPlaceholder, state}) => {
|
||||
if (isPlaceholder || state.selectedItems.length === 0) {
|
||||
return defaultChildren;
|
||||
}
|
||||
|
||||
const selectedItem = users.find((user) => user.id === state.selectedItems[0].key);
|
||||
|
||||
if (!selectedItem) {
|
||||
return defaultChildren;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="size-4" size="sm">
|
||||
<AvatarImage src={selectedItem.avatarUrl} />
|
||||
<AvatarFallback>{selectedItem.fallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{selectedItem.name}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Select.Value>
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
{/* ListBox items */}
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
The Select component implements the ARIA listbox pattern and provides:
|
||||
|
||||
- Full keyboard navigation support
|
||||
- Screen reader announcements for selection changes
|
||||
- Proper focus management
|
||||
- Support for disabled states
|
||||
- Typeahead search functionality
|
||||
- HTML form integration
|
||||
|
||||
For more information, see the [React Aria Select documentation](https://react-spectrum.adobe.com/react-aria/Select.html).
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ links:
|
||||
rac: Separator
|
||||
source: separator/separator.tsx
|
||||
styles: separator.css
|
||||
storybook: separator
|
||||
storybook: Components/Layout/Separator
|
||||
figma: true
|
||||
---
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Skeleton is a placeholder to show a loading state and the expected
|
||||
links:
|
||||
source: skeleton/skeleton.tsx
|
||||
styles: skeleton.css
|
||||
storybook: skeleton
|
||||
storybook: Components/Feedback/Skeleton
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -45,6 +45,20 @@ import { Skeleton } from '@heroui/react';
|
||||
name="skeleton-animation-types"
|
||||
/>
|
||||
|
||||
### Grid
|
||||
|
||||
<ComponentPreview
|
||||
name="skeleton-grid"
|
||||
/>
|
||||
|
||||
### Single Shimmer
|
||||
|
||||
A synchronized shimmer effect that passes over all skeleton elements at once. Apply the `skeleton--shimmer` class to a parent container and set `animationType="none"` on child skeletons.
|
||||
|
||||
<ComponentPreview
|
||||
name="skeleton-single-shimmer"
|
||||
/>
|
||||
|
||||
## Styling
|
||||
|
||||
### Global Animation Configuration
|
||||
@@ -91,12 +105,12 @@ To customize the Skeleton component classes, you can use the `@layer components`
|
||||
@layer components {
|
||||
/* Base skeleton styles */
|
||||
.skeleton {
|
||||
@apply bg-surface-3/50; /* Change base background */
|
||||
@apply bg-surface-secondary/50; /* Change base background */
|
||||
}
|
||||
|
||||
/* Shimmer animation gradient */
|
||||
.skeleton--shimmer:before {
|
||||
@apply via-surface-2; /* Change shimmer gradient color */
|
||||
@apply viasurface; /* Change shimmer gradient color */
|
||||
}
|
||||
|
||||
/* Pulse animation */
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
---
|
||||
title: Slider
|
||||
description: A slider allows a user to select one or more values within a range
|
||||
icon: new
|
||||
links:
|
||||
rac: Slider
|
||||
source: slider/slider.tsx
|
||||
styles: slider.css
|
||||
storybook: Components/Controls/Slider
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { Slider } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="slider-default"
|
||||
/>
|
||||
|
||||
### Anatomy
|
||||
|
||||
Import the Slider component and access all parts using dot notation.
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Slider>
|
||||
<Label />
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
)
|
||||
```
|
||||
|
||||
### Range Slider Anatomy
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Slider defaultValue={[25, 75]}>
|
||||
<Label />
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
{({state}) => (
|
||||
<>
|
||||
<Slider.Fill />
|
||||
{state.values.map((_, i) => (
|
||||
<Slider.Thumb key={i} index={i} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
)
|
||||
```
|
||||
|
||||
### Vertical
|
||||
|
||||
<ComponentPreview
|
||||
name="slider-vertical"
|
||||
/>
|
||||
|
||||
### Range
|
||||
|
||||
<ComponentPreview
|
||||
name="slider-range"
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
<ComponentPreview
|
||||
name="slider-disabled"
|
||||
/>
|
||||
|
||||
<RelatedComponents component="slider" />
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
function CustomSlider() {
|
||||
return (
|
||||
<Slider className="w-full">
|
||||
<Label>Volume</Label>
|
||||
<Slider.Output className="text-muted-fg text-sm" />
|
||||
<Slider.Track className="h-2 rounded-full bg-surface-secondary">
|
||||
<Slider.Fill className="bg-accent" />
|
||||
<Slider.Thumb className="size-4 rounded-full bg-accent" />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the Slider component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.slider {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.slider__output {
|
||||
@apply text-muted-fg text-sm;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
@apply relative h-2 w-full rounded-full bg-surface-secondary;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
@apply absolute h-full rounded-full bg-accent;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
@apply size-4 rounded-full bg-accent border-2 border-background;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The Slider component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/slider.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.slider` - Base slider container
|
||||
- `.slider__output` - Output element displaying current value(s)
|
||||
- `.slider-track` - Track element containing fill and thumbs
|
||||
- `.slider-fill` - Fill element showing selected range
|
||||
- `.slider-thumb` - Individual thumb element
|
||||
|
||||
#### State Classes
|
||||
- `.slider[data-disabled="true"]` - Disabled slider state
|
||||
- `.slider[data-orientation="vertical"]` - Vertical orientation
|
||||
- `.slider-thumb[data-dragging="true"]` - Thumb being dragged
|
||||
- `.slider-thumb[data-focus-visible="true"]` - Thumb keyboard focused
|
||||
- `.slider-thumb[data-disabled="true"]` - Disabled thumb state
|
||||
- `.slider-track[data-fill-start="true"]` - Fill starts at beginning
|
||||
- `.slider-track[data-fill-end="true"]` - Fill ends at end
|
||||
|
||||
### Interactive States
|
||||
|
||||
The component supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
- **Hover**: `:hover` or `[data-hovered="true"]` on thumb
|
||||
- **Focus**: `:focus-visible` or `[data-focus-visible="true"]` on thumb
|
||||
- **Dragging**: `[data-dragging="true"]` on thumb
|
||||
- **Disabled**: `:disabled` or `[data-disabled="true"]` on slider or thumb
|
||||
|
||||
## API Reference
|
||||
|
||||
### Slider Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `number \| number[]` | - | The current value (controlled) |
|
||||
| `defaultValue` | `number \| number[]` | - | The default value (uncontrolled) |
|
||||
| `onChange` | `(value: number \| number[]) => void` | - | Handler called when the value changes |
|
||||
| `onChangeEnd` | `(value: number \| number[]) => void` | - | Handler called when dragging ends |
|
||||
| `minValue` | `number` | `0` | The slider's minimum value |
|
||||
| `maxValue` | `number` | `100` | The slider's maximum value |
|
||||
| `step` | `number` | `1` | The slider's step value |
|
||||
| `formatOptions` | `Intl.NumberFormatOptions` | - | The display format of the value label |
|
||||
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | The orientation of the slider |
|
||||
| `isDisabled` | `boolean` | - | Whether the slider is disabled |
|
||||
| `aria-label` | `string` | - | Accessibility label for the slider |
|
||||
| `aria-labelledby` | `string` | - | ID of element that labels the slider |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Slider content or render function |
|
||||
|
||||
### Slider.Output Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Output content or render function |
|
||||
|
||||
### Slider.Track Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Track content or render function |
|
||||
|
||||
### Slider.Fill Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `style` | `CSSProperties` | - | Inline styles |
|
||||
|
||||
### Slider.Thumb Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `index` | `number` | `0` | Index of the thumb within the slider |
|
||||
| `isDisabled` | `boolean` | - | Whether this thumb is disabled |
|
||||
| `name` | `string` | - | The name of the input element, used when submitting an HTML form |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode \| RenderFunction` | - | Thumb content or render function |
|
||||
|
||||
### RenderProps
|
||||
|
||||
When using render functions with Slider.Output or Slider.Track, these values are provided:
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `state` | `SliderState` | The state of the slider |
|
||||
| `values` | `number[]` | Values managed by the slider by thumb index |
|
||||
| `getThumbValueLabel` | `(index: number) => string` | Returns the string label for the specified thumb's value |
|
||||
| `orientation` | `"horizontal" \| "vertical"` | The orientation of the slider |
|
||||
| `isDisabled` | `boolean` | Whether the slider is disabled |
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
<Slider defaultValue={30}>
|
||||
<Label>Volume</Label>
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
```
|
||||
|
||||
### Range Slider
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
<Slider
|
||||
defaultValue={[100, 500]}
|
||||
formatOptions={{style: "currency", currency: "USD"}}
|
||||
maxValue={1000}
|
||||
minValue={0}
|
||||
step={50}
|
||||
>
|
||||
<Label>Price Range</Label>
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
{({state}) => (
|
||||
<>
|
||||
<Slider.Fill />
|
||||
{state.values.map((_, i) => (
|
||||
<Slider.Thumb key={i} index={i} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
```
|
||||
|
||||
### Controlled Value
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
function ControlledSlider() {
|
||||
const [value, setValue] = useState(25);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Slider value={value} onChange={setValue}>
|
||||
<Label>Volume</Label>
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
<p>Current value: {value}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Value Formatting
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
<Slider
|
||||
defaultValue={60}
|
||||
formatOptions={{style: "currency", currency: "USD"}}
|
||||
>
|
||||
<Label>Price</Label>
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
```
|
||||
|
||||
### Vertical Orientation
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
<Slider defaultValue={30} orientation="vertical" aria-label="Volume">
|
||||
<Label>Volume</Label>
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
```
|
||||
|
||||
### Custom Output Display
|
||||
|
||||
```tsx
|
||||
import { Slider, Label } from '@heroui/react';
|
||||
|
||||
<Slider defaultValue={[25, 75]}>
|
||||
<Label>Range</Label>
|
||||
<Slider.Output>
|
||||
{({state}) =>
|
||||
state.values.map((_, i) => state.getThumbValueLabel(i)).join(' – ')
|
||||
}
|
||||
</Slider.Output>
|
||||
<Slider.Track>
|
||||
{({state}) => (
|
||||
<>
|
||||
<Slider.Fill />
|
||||
{state.values.map((_, i) => (
|
||||
<Slider.Thumb key={i} index={i} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
The Slider component implements the ARIA slider pattern and provides:
|
||||
|
||||
- Full keyboard navigation support (Arrow keys, Home, End, Page Up/Down)
|
||||
- Screen reader announcements for value changes
|
||||
- Proper focus management
|
||||
- Support for disabled states
|
||||
- HTML form integration via hidden input elements
|
||||
- Internationalization support with locale-aware value formatting
|
||||
- Right-to-left (RTL) language support
|
||||
|
||||
For more information, see the [React Aria Slider documentation](https://react-spectrum.adobe.com/react-aria/Slider.html).
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A loading indicator component to show pending states
|
||||
links:
|
||||
source: spinner/spinner.tsx
|
||||
styles: spinner.css
|
||||
storybook: spinner
|
||||
storybook: Components/Feedback/Spinner
|
||||
figma: true
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
title: Surface
|
||||
description: Container component that provides surface-level styling and context for child components
|
||||
icon: new
|
||||
links:
|
||||
source: surface/surface.tsx
|
||||
styles: surface.css
|
||||
storybook: surface
|
||||
---
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { Surface } from '@heroui/react';
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<ComponentPreview
|
||||
name="surface-variants"
|
||||
/>
|
||||
|
||||
## Overview
|
||||
|
||||
The Surface component is a semantic container that provides different levels of visual prominence through variants. It also exposes a context that child components (like Input, TextArea, RadioGroup, and InputOTP) can use to automatically apply on-surface styling.
|
||||
|
||||
### Variants
|
||||
|
||||
Surface comes in semantic variants that describe their prominence level:
|
||||
|
||||
- **`default`** - Standard surface appearance (bg-surface)
|
||||
- **`secondary`** - Medium prominence (bg-surface-secondary)
|
||||
- **`tertiary`** - Higher prominence (bg-surface-tertiary)
|
||||
- **`quaternary`** - Highest prominence (bg-surface-quaternary)
|
||||
|
||||
## Context
|
||||
|
||||
The Surface component provides a `SurfaceContext` that child components can access via `useContext(SurfaceContext)`. Form components like Input, TextArea, RadioGroup, and InputOTP automatically detect when they're inside a Surface and apply the appropriate on-surface styling.
|
||||
|
||||
```tsx
|
||||
import { Surface, Input, TextArea } from '@heroui/react';
|
||||
import { useContext } from 'react';
|
||||
import { SurfaceContext } from '@heroui/react';
|
||||
|
||||
function MyComponent() {
|
||||
const surfaceContext = useContext(SurfaceContext);
|
||||
// Access the variant if needed
|
||||
const variant = surfaceContext.variant;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input placeholder="This input will use on-surface styling" />
|
||||
<TextArea placeholder="This textarea will use on-surface styling" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Surface variant="default">
|
||||
<MyComponent />
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
### Passing Tailwind CSS classes
|
||||
|
||||
```tsx
|
||||
import { Surface } from '@heroui/react';
|
||||
|
||||
function CustomSurface() {
|
||||
return (
|
||||
<Surface
|
||||
className="rounded-2xl p-8 shadow-lg"
|
||||
variant="secondary"
|
||||
>
|
||||
<h2>Custom Styled Surface</h2>
|
||||
<p>Content goes here</p>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing the component classes
|
||||
|
||||
To customize the Surface component classes, you can use the `@layer components` directive.
|
||||
<br/>[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes).
|
||||
|
||||
```css
|
||||
@layer components {
|
||||
.surface {
|
||||
@apply rounded-2xl border border-border;
|
||||
}
|
||||
|
||||
.surface--secondary {
|
||||
@apply bg-gradient-to-br from-blue-50 to-purple-50;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize.
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The Surface component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/surface.css)):
|
||||
|
||||
#### Base Classes
|
||||
- `.surface` - Base surface container
|
||||
|
||||
#### Variant Classes
|
||||
- `.surface--default` - Default surface variant (bg-surface)
|
||||
- `.surface--secondary` - Secondary surface variant (bg-surface-secondary)
|
||||
- `.surface--tertiary` - Tertiary surface variant (bg-surface-tertiary)
|
||||
- `.surface--quaternary` - Quaternary surface variant (bg-surface-quaternary)
|
||||
|
||||
## API Reference
|
||||
|
||||
### Surface Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | `"default" \| "secondary" \| "tertiary" \| "quaternary"` | `"default"` | The visual variant of the surface |
|
||||
| `asChild` | `boolean` | `false` | Merge props onto the child element |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
| `children` | `ReactNode` | - | The surface content |
|
||||
|
||||
## Context API
|
||||
|
||||
### SurfaceContext
|
||||
|
||||
Child components can access the Surface context to get the current variant:
|
||||
|
||||
```tsx
|
||||
import { useContext } from 'react';
|
||||
import { SurfaceContext } from '@heroui/react';
|
||||
|
||||
function MyComponent() {
|
||||
const { variant } = useContext(SurfaceContext);
|
||||
// variant will be "default" | "secondary" | "tertiary" | "quaternary" | undefined
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Switch
|
||||
description: A toggle switch component for boolean states
|
||||
icon: updated
|
||||
links:
|
||||
rac: Switch
|
||||
source: switch/switch.tsx
|
||||
styles: switch.css
|
||||
storybook: switch
|
||||
storybook: Components/Controls/Switch
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -30,14 +29,14 @@ Import the Switch component and access all parts using dot notation.
|
||||
import { Switch, Label } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Switch.Root>
|
||||
<Switch>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb>
|
||||
<Switch.Icon/> {/* Optional */}
|
||||
</Switch.Thumb>
|
||||
</Switch.Control>
|
||||
<Label/> {/* Optional */}
|
||||
</Switch.Root>
|
||||
</Switch>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -48,18 +47,18 @@ import { Switch, SwitchGroup, Label } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<SwitchGroup>
|
||||
<Switch.Root>
|
||||
<Switch>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Label>Option 1</Label>
|
||||
</Switch.Root>
|
||||
<Switch.Root>
|
||||
</Switch>
|
||||
<Switch>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Label>Option 2</Label>
|
||||
</Switch.Root>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
);
|
||||
```
|
||||
@@ -153,7 +152,7 @@ import { Switch, Label } from '@heroui/react';
|
||||
|
||||
function CustomSwitch() {
|
||||
return (
|
||||
<Switch.Root>
|
||||
<Switch>
|
||||
{({isSelected}) => (
|
||||
<>
|
||||
<Switch.Control
|
||||
@@ -166,7 +165,7 @@ function CustomSwitch() {
|
||||
<Label>Custom Switch</Label>
|
||||
</>
|
||||
)}
|
||||
</Switch.Root>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -179,18 +178,18 @@ import { Switch, SwitchGroup, Label } from '@heroui/react';
|
||||
function CustomSwitchGroup() {
|
||||
return (
|
||||
<SwitchGroup className="gap-8" orientation="horizontal">
|
||||
<Switch.Root>
|
||||
<Switch>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Label>Option 1</Label>
|
||||
</Switch.Root>
|
||||
<Switch.Root>
|
||||
</Switch>
|
||||
<Switch>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Label>Option 2</Label>
|
||||
</Switch.Root>
|
||||
</Switch>
|
||||
</SwitchGroup>
|
||||
);
|
||||
}
|
||||
@@ -258,7 +257,7 @@ The switch supports both CSS pseudo-classes and data attributes for flexibility:
|
||||
|
||||
## API Reference
|
||||
|
||||
### Switch.Root Props
|
||||
### Switch Props
|
||||
|
||||
Inherits from [React Aria Switch](https://react-spectrum.adobe.com/react-aria/Switch.html).
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Tabs
|
||||
description: Tabs organize content into multiple sections and allow users to navigate between them.
|
||||
icon: updated
|
||||
links:
|
||||
rac: Tabs
|
||||
source: tabs/tabs.tsx
|
||||
styles: tabs.css
|
||||
storybook: tabs
|
||||
storybook: Components/Navigation/Tabs
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -31,16 +30,16 @@ Import the Tabs component and access all parts using dot notation.
|
||||
import { Tabs } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Tabs.Root>
|
||||
<Tabs.ListWrapper>
|
||||
<Tabs>
|
||||
<Tabs.ListContainer>
|
||||
<Tabs.List aria-label="Options">
|
||||
<Tabs.Tab>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListWrapper>
|
||||
</Tabs.ListContainer>
|
||||
<Tabs.Panel/>
|
||||
</Tabs.Root>
|
||||
</Tabs>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -76,8 +75,8 @@ import { Tabs } from '@heroui/react';
|
||||
|
||||
function CustomTabs() {
|
||||
return (
|
||||
<Tabs.Root className="w-full max-w-lg text-center">
|
||||
<Tabs.ListWrapper>
|
||||
<Tabs className="w-full max-w-lg text-center">
|
||||
<Tabs.ListContainer>
|
||||
<Tabs.List
|
||||
aria-label="Options"
|
||||
className="*:data-[selected=true]:text-accent-foreground w-fit *:h-6 *:w-fit *:px-3 *:text-sm *:font-normal"
|
||||
@@ -87,7 +86,7 @@ function CustomTabs() {
|
||||
<Tabs.Tab id="bi-weekly">Bi-Weekly<Tabs.Indicator /></Tabs.Tab>
|
||||
<Tabs.Tab id="monthly">Monthly<Tabs.Indicator /></Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListWrapper>
|
||||
</Tabs.ListContainer>
|
||||
<Tabs.Panel className="px-4" id="daily">
|
||||
<h3 className="mb-2 font-semibold">Daily</h3>
|
||||
<p className="text-sm text-gray-600">Manage your daily tasks and goals.</p>
|
||||
@@ -104,7 +103,7 @@ function CustomTabs() {
|
||||
<h3 className="mb-2 font-semibold">Monthly</h3>
|
||||
<p className="text-sm text-gray-600">Manage your monthly tasks and goals.</p>
|
||||
</Tabs.Panel>
|
||||
</Tabs.Root>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
`} />
|
||||
@@ -115,7 +114,7 @@ The Tabs component uses these CSS classes:
|
||||
|
||||
#### Base Classes
|
||||
- `.tabs` - Base tabs container
|
||||
- `.tabs__list-wrapper` - Tab list wrapper
|
||||
- `.tabs__list-container` - Tab list container wrapper
|
||||
- `.tabs__list` - Tab list container
|
||||
- `.tabs__tab` - Individual tab button
|
||||
- `.tabs__panel` - Tab panel content
|
||||
@@ -138,7 +137,7 @@ The component supports both CSS pseudo-classes and data attributes:
|
||||
|
||||
## API Reference
|
||||
|
||||
### Tabs.Root Props
|
||||
### Tabs Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: TextField
|
||||
description: Composition-friendly text fields with labels, descriptions, and inline validation
|
||||
icon: updated
|
||||
links:
|
||||
rac: TextField
|
||||
source: text-field/text-field.tsx
|
||||
styles: text-field.css
|
||||
storybook: text-field
|
||||
storybook: Components/Forms/TextField
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -24,16 +25,14 @@ import { TextField } from '@heroui/react';
|
||||
```tsx
|
||||
import {TextField, Label, Input, Description, FieldError} from '@heroui/react';
|
||||
|
||||
export default function Example() {
|
||||
return (
|
||||
<TextField isRequired>
|
||||
<Label>Display name</Label>
|
||||
<Input placeholder="Jane" />
|
||||
<Description>Visible to other team members.</Description>
|
||||
export default () => (
|
||||
<TextField>
|
||||
<Label />
|
||||
<Input />
|
||||
<Description />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> **TextField** combines label, input, description, and error into a single accessible component.
|
||||
@@ -77,6 +76,12 @@ Use [TextArea](/docs/components/textarea) instead of [Input](/docs/components/in
|
||||
|
||||
<ComponentPreview name="text-field-input-types" />
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, TextField and its child Input/TextArea components automatically apply on-surface styling.
|
||||
|
||||
<ComponentPreview name="text-field-on-surface" />
|
||||
|
||||
<RelatedComponents component="textfield" />
|
||||
|
||||
<RelatedShowcases component="TextField" />
|
||||
@@ -90,11 +95,11 @@ import {TextField, Label, Input, Description} from '@heroui/react';
|
||||
|
||||
function CustomTextField() {
|
||||
return (
|
||||
<TextField className="gap-2 rounded-xl border border-border/60 bg-surface-2 p-4 shadow-sm">
|
||||
<TextField className="gap-2 rounded-xl border border-border/60 bgsurface p-4 shadow-sm">
|
||||
<Label className="text-sm font-semibold text-default-700">
|
||||
Project name
|
||||
</Label>
|
||||
<Input className="rounded-lg border border-border/60 bg-surface-2 px-3 py-2" />
|
||||
<Input className="rounded-lg border border-border/60 bgsurface px-3 py-2" />
|
||||
<Description className="text-xs text-default-500">
|
||||
Keep it short and memorable.
|
||||
</Description>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: TextArea
|
||||
description: Primitive multiline text input component that accepts standard HTML attributes
|
||||
icon: updated
|
||||
links:
|
||||
rac: TextArea
|
||||
source: textarea/textarea.tsx
|
||||
styles: textarea.css
|
||||
storybook: textarea
|
||||
storybook: Components/Forms/Textarea
|
||||
---
|
||||
|
||||
## Import
|
||||
@@ -30,6 +31,12 @@ import { TextArea } from '@heroui/react';
|
||||
|
||||
<ComponentPreview name="textarea-rows" />
|
||||
|
||||
### On Surface
|
||||
|
||||
When used inside a [Surface](/docs/components/surface) component, TextArea automatically applies on-surface styling.
|
||||
|
||||
<ComponentPreview name="textarea-on-surface" />
|
||||
|
||||
<RelatedComponents component="textarea" />
|
||||
|
||||
<RelatedShowcases component="TextArea" />
|
||||
@@ -47,7 +54,7 @@ function CustomTextArea() {
|
||||
<Label htmlFor="custom-textarea">Message</Label>
|
||||
<TextArea
|
||||
id="custom-textarea"
|
||||
className="rounded-xl border border-border/70 bg-surface-2 px-4 py-3 text-sm leading-6 shadow-sm"
|
||||
className="rounded-xl border border-border/70 bgsurface px-4 py-3 text-sm leading-6 shadow-sm"
|
||||
placeholder="Let us know how we can help..."
|
||||
rows={5}
|
||||
style={{resize: "vertical"}}
|
||||
@@ -64,11 +71,11 @@ Override the shared `.textarea` class once with Tailwind's `@layer components`.
|
||||
```css
|
||||
@layer components {
|
||||
.textarea {
|
||||
@apply rounded-xl border border-border bg-surface-2 px-4 py-3 text-sm leading-6 shadow-sm;
|
||||
@apply rounded-xl border border-border bgsurface px-4 py-3 text-sm leading-6 shadow-sm;
|
||||
|
||||
&:hover,
|
||||
&[data-hovered="true"] {
|
||||
@apply bg-surface-3 border-border/80;
|
||||
@apply bg-surface-secondary border-border/80;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
@@ -117,5 +124,6 @@ TextArea accepts all standard HTML `<textarea>` attributes plus the following:
|
||||
| `maxLength` | `number` | - | Maximum number of characters. |
|
||||
| `minLength` | `number` | - | Minimum number of characters. |
|
||||
| `wrap` | `'soft' \| 'hard'` | - | How text wraps when submitted. |
|
||||
| `isOnSurface` | `boolean` | `false` | Whether the textarea is displayed on a surface (affects styling) |
|
||||
|
||||
> For validation props like `isInvalid`, `isRequired`, and error handling, use **[TextField](/docs/components/text-field)** with TextArea as a child component.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
title: Tooltip
|
||||
description: Displays informative text when users hover over or focus on an element
|
||||
icon: updated
|
||||
links:
|
||||
rac: Tooltip
|
||||
source: tooltip/tooltip.tsx
|
||||
styles: tooltip.css
|
||||
storybook: tooltip
|
||||
storybook: Components/Overlays/Tooltip
|
||||
figma: true
|
||||
---
|
||||
|
||||
@@ -30,7 +29,7 @@ Import the Tooltip component and access all parts using dot notation.
|
||||
import { Tooltip } from '@heroui/react';
|
||||
|
||||
export default () => (
|
||||
<Tooltip.Root>
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>
|
||||
<button>Hover for tooltip</button>
|
||||
</Tooltip.Trigger>
|
||||
@@ -38,7 +37,7 @@ export default () => (
|
||||
<Tooltip.Arrow />
|
||||
Helpful information about this element
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -69,14 +68,14 @@ import { Tooltip, Button } from '@heroui/react';
|
||||
|
||||
function CustomTooltip() {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>
|
||||
<Button>Hover me</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content className="bg-accent text-accent-foreground">
|
||||
<p>Custom styled tooltip</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -118,7 +117,7 @@ The component supports animation states:
|
||||
|
||||
## API Reference
|
||||
|
||||
### Tooltip.Root Props
|
||||
### Tooltip Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
|
||||
@@ -17,8 +17,8 @@ Components use semantic naming (primary, secondary, tertiary) instead of visual
|
||||
Inspired by [Uber's Base design system](https://base.uber.com/6d2425e9f/p/756216-button), our variants follow a clear hierarchy:
|
||||
|
||||
<DocsImage
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/semantic-intent.jpg"
|
||||
darkSrc="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/semantic-intent-dark.jpg"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/emphasis.jpg"
|
||||
darkSrc="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/emphasis-dark.jpg"
|
||||
alt="Semantic Intent Hierarchy"
|
||||
/>
|
||||
|
||||
@@ -124,10 +124,10 @@ import { Alert, AlertIcon, CardHeader, AccordionTrigger } from '@heroui/react';
|
||||
<AlertIcon />
|
||||
</Alert>
|
||||
|
||||
// Dot notation (requires .Root)
|
||||
<Alert.Root>
|
||||
// Dot notation
|
||||
<Alert>
|
||||
<Alert.Icon />
|
||||
</Alert.Root>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
### 6. Type Safety First
|
||||
|
||||
@@ -172,17 +172,25 @@ Use render props to apply dynamic animations based on component state:
|
||||
|
||||
### Respecting Motion Preferences
|
||||
|
||||
Always respect user motion preferences:
|
||||
HeroUI components automatically respect user motion preferences using Tailwind's `motion-reduce:` utility. All built-in transitions and animations are disabled when users enable "reduce motion" in their system settings.
|
||||
|
||||
HeroUI extends Tailwind's `motion-reduce:` variant to support both the native `prefers-reduced-motion` media query and the `data-reduce-motion` attribute, providing flexible control over animations.
|
||||
|
||||
```css
|
||||
/* HeroUI pattern - uses Tailwind's motion-reduce: */
|
||||
.button {
|
||||
@apply transition-colors motion-reduce:transition-none;
|
||||
}
|
||||
|
||||
/* Expands to support both approaches: */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.button {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-* {
|
||||
animation: none !important;
|
||||
}
|
||||
[data-reduce-motion="true"] .button {
|
||||
transition: none;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -206,6 +214,19 @@ function AnimatedCard() {
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling Animations Globally
|
||||
|
||||
Disable all animations by adding `data-reduce-motion="true"` to the `<html>` or `<body>` tag:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html data-reduce-motion="true">
|
||||
<!-- All HeroUI animations will be disabled -->
|
||||
</html>
|
||||
```
|
||||
|
||||
HeroUI automatically detects the user's `prefers-reduced-motion: reduce` setting and disables animations accordingly. By extending Tailwind's native `motion-reduce:` variant, HeroUI provides a consistent API that works seamlessly with both system preferences and manual control via the data attribute.
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Use GPU-Accelerated Properties
|
||||
@@ -228,6 +249,8 @@ Prefer `transform` and `opacity` for smooth animations:
|
||||
|
||||
### Will-Change Optimization
|
||||
|
||||
Use `will-change` to optimize animations, but remove it when not animating:
|
||||
|
||||
```css
|
||||
.button {
|
||||
will-change: transform;
|
||||
|
||||
@@ -34,8 +34,6 @@ We follow a simple naming pattern:
|
||||
<div className="bg-accent text-accent-foreground">Hello</div>
|
||||
```
|
||||
|
||||
## Available Colors
|
||||
|
||||
### Base Colors
|
||||
|
||||
These four colors stay the same in all themes:
|
||||
@@ -53,11 +51,10 @@ These four colors stay the same in all themes:
|
||||
colors={[
|
||||
{ name: "Background", variable: "--background", foreground: "--foreground" },
|
||||
{ name: "Foreground", variable: "--foreground" },
|
||||
{ name: "Panel", variable: "--panel", foreground: "--panel-foreground" },
|
||||
{ name: "Panel Foreground", variable: "--panel-foreground" },
|
||||
{ name: "Surface 1", variable: "--surface-1", foreground: "--foreground" },
|
||||
{ name: "Surface 2", variable: "--surface-2", foreground: "--foreground" },
|
||||
{ name: "Surface 3", variable: "--surface-3", foreground: "--foreground" },
|
||||
{ name: "Surface", variable: "--surface", foreground: "--surface-foreground" },
|
||||
{ name: "Surface Foreground", variable: "--surface-foreground" },
|
||||
{ name: "Overlay", variable: "--overlay", foreground: "--overlay-foreground" },
|
||||
{ name: "Overlay Foreground", variable: "--overlay-foreground" },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -103,8 +100,6 @@ For consistent form field styling across input components:
|
||||
]}
|
||||
/>
|
||||
|
||||
> **Note**: These variables default to existing colors but can be overridden for specialized form styling without affecting other components.
|
||||
|
||||
### Other Colors
|
||||
|
||||
<ColorPalette
|
||||
@@ -163,107 +158,98 @@ For consistent form field styling across input components:
|
||||
|
||||
## Default Theme
|
||||
|
||||
The complete theme definition can be found in ([default.css](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/index.css)). This theme automatically switches between light and dark modes based on the `class="dark"` or `data-theme="dark"` attributes.
|
||||
The complete theme definition can be found in ([variables.css](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/variables.css)). This theme automatically switches between light and dark modes based on the `class="dark"` or `data-theme="dark"` attributes.
|
||||
|
||||
<CollapsibleCode lang="css" code={` @layer base {
|
||||
/* HeroUI Default Theme */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* == Common Variables == */
|
||||
|
||||
/* Primitive Colors (Do not change between light and dark) */
|
||||
--white: oklch(100% 0 0);
|
||||
--black: oklch(0% 0 0);
|
||||
--snow: oklch(0.9911 0 0);
|
||||
--eclipse: oklch(0.2221 0 0);
|
||||
--eclipse: oklch(0.2103 0.0059 285.89);
|
||||
|
||||
/* Spacing scale */
|
||||
--spacing: 0.25rem;
|
||||
|
||||
/* Border */
|
||||
--border-width: 1px;
|
||||
--border-width: 0px; /* no border by default */
|
||||
--field-border-width: var(--border-width);
|
||||
--disabled-opacity: 0.5;
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.5rem;
|
||||
--field-radius: calc(var(--radius) * 1.5);
|
||||
|
||||
/* Panel specific radius */
|
||||
--radius-panel: 0.5rem;
|
||||
--radius-panel-inner: calc(var(--radius-panel) * 0.5);
|
||||
|
||||
/* Small radius for compact components */
|
||||
--radius-sm: 0.375rem;
|
||||
|
||||
/* Ring offset - Used for focus ring */
|
||||
--ring-offset-width: 2px;
|
||||
|
||||
/* Typography */
|
||||
--font-size-base: 1rem;
|
||||
--font-size-scale-desktop: 0.875;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
|
||||
/* Cursor */
|
||||
--cursor-interactive: pointer;
|
||||
--cursor-disabled: not-allowed;
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.5rem;
|
||||
--field-radius: calc(var(--radius) * 1.5);
|
||||
|
||||
/* == Light Theme Variables == */
|
||||
|
||||
/* Base Colors */
|
||||
--background: var(--white);
|
||||
--background: oklch(0.9702 0 0);
|
||||
--foreground: var(--eclipse);
|
||||
--panel: var(--white);
|
||||
--panel-foreground: var(--foreground);
|
||||
|
||||
--radius: 1rem;
|
||||
/* Surface: Used for non-overlay components (cards, accordions, disclosure groups) */
|
||||
--surface: var(--white);
|
||||
--surface-foreground: var(--foreground);
|
||||
|
||||
--muted: var(--color-neutral-500);
|
||||
/* Overlay: Used for floating/overlay components (tooltips, popovers, modals, menus) */
|
||||
--overlay: var(--white);
|
||||
--overlay-foreground: var(--foreground);
|
||||
|
||||
--scrollbar: var(--color-neutral-300);
|
||||
--muted: oklch(0.5517 0.0138 285.94);
|
||||
--scrollbar: oklch(87.1% 0.006 286.286);
|
||||
|
||||
--default: var(--color-neutral-50);
|
||||
--default: oklch(94% 0.001 286.375);
|
||||
--default-foreground: var(--eclipse);
|
||||
|
||||
--surface-1: var(--background);
|
||||
--surface-2: var(--color-neutral-100);
|
||||
--surface-3: var(--color-neutral-200);
|
||||
|
||||
--accent: var(--color-neutral-950);
|
||||
--accent: oklch(0.6204 0.195 253.83);
|
||||
--accent-foreground: var(--snow);
|
||||
|
||||
--accent-soft: var(--color-neutral-200);
|
||||
--accent-soft-foreground: var(--color-neutral-900);
|
||||
|
||||
/* Form Field Defaults */
|
||||
--field-background: var(--default);
|
||||
--field-foreground: var(--foreground);
|
||||
/* Form Field Defaults - Colors */
|
||||
--field-background: var(--white);
|
||||
--field-foreground: oklch(0.2103 0.0059 285.89);
|
||||
--field-placeholder: var(--muted);
|
||||
--field-border: var(--border);
|
||||
--field-border: transparent; /* no border by default on form fields */
|
||||
|
||||
/* Status Colors */
|
||||
--success: oklch(0.5503 0.1244 153.56);
|
||||
--success-foreground: var(--snow);
|
||||
--success: oklch(0.7329 0.1935 150.81);
|
||||
--success-foreground: var(--eclipse);
|
||||
|
||||
--warning: oklch(0.7186 0.1521 64.85);
|
||||
--warning: oklch(0.7819 0.1585 72.33);
|
||||
--warning-foreground: var(--eclipse);
|
||||
|
||||
--danger: oklch(0.6259 0.1908 29.19);
|
||||
--danger: oklch(0.6532 0.2328 25.74);
|
||||
--danger-foreground: var(--snow);
|
||||
|
||||
/* Component Colors */
|
||||
--segment: var(--white);
|
||||
--segment-foreground: var(--foreground);
|
||||
--segment-foreground: var(--eclipse);
|
||||
|
||||
/* Misc Colors */
|
||||
--border: oklch(0 0 0 / 15%);
|
||||
--divider: var(--color-neutral-200);
|
||||
--focus: oklch(0% 0 0 / 20%);
|
||||
--border: oklch(0 0 0 / 0%);
|
||||
--divider: oklch(92% 0.004 286.32);
|
||||
--focus: var(--accent);
|
||||
--link: var(--foreground);
|
||||
|
||||
/* Shadows */
|
||||
--panel-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.3) inset, 0 2px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
/* Shadow used for form fields */
|
||||
--field-shadow: 0 0 0 0 rgba(255, 255, 255, 0.1) inset, 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
--surface-shadow:
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
|
||||
0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
/* Overlay shadow */
|
||||
--overlay-shadow: 0 4px 16px 0 rgba(24, 24, 27, 0.08), 0 8px 24px 0 rgba(24, 24, 27, 0.09);
|
||||
--field-shadow:
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
|
||||
0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
/* Skeleton Default Global Animation */
|
||||
--skeleton-animation: shimmer; /* shimmer, pulse, none */
|
||||
}
|
||||
@@ -271,62 +257,77 @@ The complete theme definition can be found in ([default.css](https://github.com/
|
||||
.dark,
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
/* == Dark Theme Variables == */
|
||||
|
||||
/* Base Colors */
|
||||
--background: var(--black);
|
||||
--background: oklch(12% 0.005 285.823);
|
||||
--foreground: var(--snow);
|
||||
--panel: var(--eclipse);
|
||||
|
||||
--muted: var(--color-neutral-400);
|
||||
/* Surface: Used for non-overlay components (cards, accordions, disclosure groups) */
|
||||
--surface: oklch(0.2103 0.0059 285.89);
|
||||
--surface-foreground: var(--foreground);
|
||||
|
||||
--scrollbar: var(--color-neutral-800);
|
||||
/* Overlay: Used for floating/overlay components (tooltips, popovers, modals, menus) - lighter for contrast */
|
||||
--overlay: oklch(0.22 0.0059 285.89); /* A bit lighter than surface for visibility in dark mode */
|
||||
--overlay-foreground: var(--foreground);
|
||||
|
||||
--default: var(--color-neutral-900);
|
||||
--muted: oklch(70.5% 0.015 286.067);
|
||||
--scrollbar: oklch(70.5% 0.015 286.067);
|
||||
|
||||
--default: oklch(27.4% 0.006 286.033);
|
||||
--default-foreground: var(--snow);
|
||||
|
||||
--surface-1: var(--background);
|
||||
--surface-2: var(--color-neutral-900);
|
||||
--surface-3: var(--color-neutral-800);
|
||||
|
||||
--accent: var(--color-neutral-50);
|
||||
--accent-foreground: var(--eclipse);
|
||||
|
||||
--accent-soft: var(--color-neutral-800);
|
||||
--accent-soft-foreground: var(--color-neutral-200);
|
||||
/* Form Field Defaults - Colors (only the ones that are different from light theme) */
|
||||
--field-background: var(--default);
|
||||
--field-foreground: var(--foreground);
|
||||
|
||||
/* Status Colors */
|
||||
--success: oklch(0.5503 0.1244 153.56);
|
||||
--success-foreground: var(--snow);
|
||||
|
||||
--warning: oklch(85% 0.1414 77.88);
|
||||
--warning: oklch(0.8203 0.1388 76.34);
|
||||
--warning-foreground: var(--eclipse);
|
||||
|
||||
--danger: oklch(0.5798 0.1635 30.83);
|
||||
--danger: oklch(0.594 0.1967 24.63);
|
||||
--danger-foreground: var(--snow);
|
||||
|
||||
/* Component Colors */
|
||||
--segment: oklch(1 0 0 / 16%);
|
||||
--segment: oklch(0.3964 0.01 285.93);
|
||||
--segment-foreground: var(--foreground);
|
||||
|
||||
/* Misc Colors */
|
||||
--border: oklch(1 0 0 / 12%);
|
||||
--divider: var(--color-neutral-800);
|
||||
--focus: oklch(100% 0 0 / 50%);
|
||||
--border: oklch(1 0 0 / 0%);
|
||||
--divider: oklch(22% 0.006 286.033);
|
||||
--focus: var(--accent);
|
||||
--link: var(--foreground);
|
||||
|
||||
/* Shadows */
|
||||
--panel-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.3) inset, 0 2px 8px 0 rgba(0, 0, 0, 0.08);
|
||||
--field-shadow: 0 0 0 0 rgba(255, 255, 255, 0.1) inset, 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--surface-shadow: 0 0 0 0 transparent inset; /* No shadow on dark mode */
|
||||
--overlay-shadow: 0 0 0 0 transparent inset; /* No shadow on dark mode */
|
||||
--field-shadow: 0 0 0 0 transparent inset; /* Transparent shadow to allow ring utilities to work */
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
> **Note**: Colors like `--color-neutral-*`, `--color-white`, and `--color-black` come from [Tailwind CSS v4's built-in theme](https://tailwindcss.com/docs/colors).
|
||||
|
||||
|
||||
## Customizing Colors
|
||||
|
||||
### Override Existing Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Override default colors */
|
||||
--accent: oklch(0.7 0.15 250);
|
||||
--success: oklch(0.65 0.15 155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Override dark theme colors */
|
||||
--accent: oklch(0.8 0.12 250);
|
||||
--success: oklch(0.75 0.12 155);
|
||||
}
|
||||
```
|
||||
|
||||
**Tip**: Convert colors at [oklch.com](https://oklch.com)
|
||||
|
||||
### Add Your Own Colors
|
||||
|
||||
```css
|
||||
@@ -358,24 +359,6 @@ Now you can use it:
|
||||
> **Note**: To learn more about theme variables and how they work in Tailwind CSS v4, see the [Tailwind CSS Theme documentation](https://tailwindcss.com/docs/theme).
|
||||
|
||||
|
||||
### Override Existing Colors
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Override default colors */
|
||||
--accent: oklch(0.7 0.15 250);
|
||||
--success: oklch(0.65 0.15 155);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Override dark theme colors */
|
||||
--accent: oklch(0.8 0.12 250);
|
||||
--success: oklch(0.75 0.12 155);
|
||||
}
|
||||
```
|
||||
|
||||
**Tip**: Convert colors at [oklch.com](https://oklch.com)
|
||||
|
||||
## Quick Tips
|
||||
|
||||
- Always use color variables, not hard-coded values
|
||||
|
||||
@@ -38,41 +38,34 @@ import Link from 'next/link';
|
||||
These components support `asChild`:
|
||||
|
||||
- `Button` - Change button element
|
||||
- `Alert` and its parts - `Alert`, `AlertIcon`, `AlertContent`, `AlertTitle`, `AlertDescription`, `AlertAction`, `AlertClose`
|
||||
- `Avatar` and its parts - `Avatar`, `AvatarImage`, `AvatarFallback`
|
||||
- More components coming soon
|
||||
- `Alert` and its parts - `Alert` (or `Alert.Root`), `Alert.Icon`, `Alert.Content`, `Alert.Title`, `Alert.Description`, `Alert.Action`, `Alert.Close`
|
||||
- `Avatar` and its parts - `Avatar` (or `Avatar.Root`), `Avatar.Image`, `Avatar.Fallback`
|
||||
- All other HeroUI components with root elements
|
||||
|
||||
## Compound Components
|
||||
|
||||
HeroUI components are built as compound components - they export multiple parts that work together. You can use them in two ways:
|
||||
HeroUI components are built as compound components - they export multiple parts that work together. You can use them in three flexible ways:
|
||||
|
||||
### Option 1: Named Exports (Recommended)
|
||||
### Option 1: Compound Pattern (Recommended)
|
||||
|
||||
Import each part separately:
|
||||
Use the main component directly without `.Root` suffix:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertContent,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertClose
|
||||
} from '@heroui/react';
|
||||
import { Alert } from '@heroui/react';
|
||||
|
||||
<Alert>
|
||||
<AlertIcon />
|
||||
<AlertContent>
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Your changes have been saved.</AlertDescription>
|
||||
</AlertContent>
|
||||
<AlertClose />
|
||||
<Alert.Icon />
|
||||
<Alert.Content>
|
||||
<Alert.Title>Success</Alert.Title>
|
||||
<Alert.Description>Your changes have been saved.</Alert.Description>
|
||||
</Alert.Content>
|
||||
<Alert.Close />
|
||||
</Alert>
|
||||
```
|
||||
|
||||
### Option 2: Dot Notation (Compound Pattern)
|
||||
### Option 2: Compound Pattern with .Root
|
||||
|
||||
Import the main component and access parts via dot notation:
|
||||
Use the `.Root` suffix if you prefer explicit naming:
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@heroui/react';
|
||||
@@ -87,15 +80,73 @@ import { Alert } from '@heroui/react';
|
||||
</Alert.Root>
|
||||
```
|
||||
|
||||
**Note:** When using dot notation, the main component requires `.Root` suffix (e.g., `<Alert.Root>` instead of `<Alert>`).
|
||||
### Option 3: Named Exports
|
||||
|
||||
Import each part separately:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AlertRoot,
|
||||
AlertIcon,
|
||||
AlertContent,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
AlertClose
|
||||
} from '@heroui/react';
|
||||
|
||||
<AlertRoot>
|
||||
<AlertIcon />
|
||||
<AlertContent>
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Your changes have been saved.</AlertDescription>
|
||||
</AlertContent>
|
||||
<AlertClose />
|
||||
</AlertRoot>
|
||||
```
|
||||
|
||||
### Mixed Syntax
|
||||
|
||||
You can mix compound and named exports in the same component:
|
||||
|
||||
```tsx
|
||||
import { Alert, AlertTitle, AlertDescription } from '@heroui/react';
|
||||
|
||||
<Alert>
|
||||
<Alert.Icon />
|
||||
<Alert.Content>
|
||||
<AlertTitle>Success</AlertTitle>
|
||||
<AlertDescription>Your changes have been saved.</AlertDescription>
|
||||
</Alert.Content>
|
||||
<Alert.Close />
|
||||
</Alert>
|
||||
```
|
||||
|
||||
### Simple Components
|
||||
|
||||
Simple components like `Button` work the same way - no `.Root` needed:
|
||||
|
||||
```tsx
|
||||
import { Button } from '@heroui/react';
|
||||
|
||||
// Recommended - no .Root needed
|
||||
<Button>Click me</Button>
|
||||
|
||||
// Or with .Root
|
||||
<Button.Root>Click me</Button.Root>
|
||||
|
||||
// Or named export
|
||||
import { ButtonRoot } from '@heroui/react';
|
||||
<ButtonRoot>Click me</ButtonRoot>
|
||||
```
|
||||
|
||||
### Benefits of Compound Components
|
||||
|
||||
Both patterns provide:
|
||||
All three patterns provide:
|
||||
|
||||
- **Flexibility** - Arrange parts as needed
|
||||
- **Customization** - Style each part independently
|
||||
- **Control** - Add or remove parts
|
||||
- **Consistency** - Choose the pattern that fits your codebase
|
||||
|
||||
## Style Variants
|
||||
|
||||
@@ -160,14 +211,14 @@ function LinkButton({ href, children, ...props }) {
|
||||
// Icon button with tooltip
|
||||
function IconButton({ icon, label, ...props }) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>
|
||||
<Button isIconOnly {...props}>
|
||||
<Icon icon={icon} />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{label}</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -177,11 +228,11 @@ function IconButton({ icon, label, ...props }) {
|
||||
You can create your own custom variants by extending the component's variant function.
|
||||
|
||||
```tsx
|
||||
import type {ButtonProps} from "@heroui/react";
|
||||
import type {VariantProps} from "tailwind-variants";
|
||||
import type { ButtonRootProps } from "@heroui/react";
|
||||
import type { VariantProps } from "tailwind-variants";
|
||||
|
||||
import {Button, buttonVariants} from "@heroui/react";
|
||||
import {tv} from "tailwind-variants";
|
||||
import { Button, buttonVariants } from "@heroui/react";
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
const myButtonVariants = tv({
|
||||
extend: buttonVariants,
|
||||
@@ -210,11 +261,11 @@ const myButtonVariants = tv({
|
||||
});
|
||||
|
||||
type MyButtonVariants = VariantProps<typeof myButtonVariants>;
|
||||
export type MyButtonProps = Omit<ButtonProps, "className"> &
|
||||
MyButtonVariants & {className?: string};
|
||||
export type MyButtonProps = Omit<ButtonRootProps, "className"> &
|
||||
MyButtonVariants & { className?: string };
|
||||
|
||||
function CustomButton({className, radius, variant, ...props}: MyButtonProps) {
|
||||
return <Button className={myButtonVariants({className, radius, variant})} {...props} />;
|
||||
function CustomButton({ className, radius, variant, ...props }: MyButtonProps) {
|
||||
return <Button className={myButtonVariants({ className, radius, variant })} {...props} />;
|
||||
}
|
||||
|
||||
export function CustomVariants() {
|
||||
@@ -222,6 +273,28 @@ export function CustomVariants() {
|
||||
}
|
||||
```
|
||||
|
||||
### Type References
|
||||
|
||||
When working with component types, use named type imports or object-style syntax:
|
||||
|
||||
**Recommended - Named type imports:**
|
||||
```tsx
|
||||
import type { ButtonRootProps, AvatarRootProps } from "@heroui/react";
|
||||
|
||||
type MyButtonProps = ButtonRootProps;
|
||||
type MyAvatarProps = AvatarRootProps;
|
||||
```
|
||||
|
||||
**Alternative - Object-style syntax:**
|
||||
```tsx
|
||||
import { Button, Avatar } from "@heroui/react";
|
||||
|
||||
type MyButtonProps = Button["RootProps"];
|
||||
type MyAvatarProps = Avatar["RootProps"];
|
||||
```
|
||||
|
||||
**Note:** The namespace syntax `Button.RootProps` is no longer supported. Use `Button["RootProps"]` or named imports instead.
|
||||
|
||||
|
||||
## With Next.js
|
||||
|
||||
|
||||
@@ -72,39 +72,61 @@ Add a theme class to your HTML and apply colors to the body:
|
||||
[data-theme="ocean"] {
|
||||
color-scheme: light;
|
||||
|
||||
/* Base */
|
||||
/* Primitive Colors (Do not change between light and dark) */
|
||||
--white: oklch(100% 0 0);
|
||||
--black: oklch(0% 0 0);
|
||||
--snow: oklch(0.9911 0 0);
|
||||
--eclipse: oklch(0.2103 0.0059 285.89);
|
||||
|
||||
/* Spacing & Layout */
|
||||
--spacing: 0.25rem;
|
||||
--border-width: 0px;
|
||||
--field-border-width: var(--border-width);
|
||||
--disabled-opacity: 0.5;
|
||||
--ring-offset-width: 2px;
|
||||
--cursor-interactive: pointer;
|
||||
--cursor-disabled: not-allowed;
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.75rem;
|
||||
--field-radius: calc(var(--radius) * 1.5);
|
||||
|
||||
/* Base Colors */
|
||||
--background: oklch(0.985 0.015 225);
|
||||
--foreground: var(--eclipse);
|
||||
--panel: var(--white);
|
||||
--panel-foreground: var(--foreground);
|
||||
|
||||
--radius: 0.75rem;
|
||||
/* Surface: Used for non-overlay components */
|
||||
--surface: var(--white);
|
||||
--surface-foreground: var(--foreground);
|
||||
|
||||
--muted: var(--color-neutral-500);
|
||||
--scrollbar: var(--color-neutral-300);
|
||||
/* Overlay: Used for floating/overlay components */
|
||||
--overlay: var(--white);
|
||||
--overlay-foreground: var(--foreground);
|
||||
|
||||
--default: var(--color-white);
|
||||
--default-foreground: var(--color-neutral-600);
|
||||
--muted: oklch(0.5517 0.0138 285.94);
|
||||
--scrollbar: oklch(87.1% 0.006 286.286);
|
||||
|
||||
--surface-1: var(--background);
|
||||
--surface-2: oklch(0.965 0.020 225);
|
||||
--surface-3: oklch(0.930 0.030 225);
|
||||
--default: oklch(94% 0.001 286.375);
|
||||
--default-foreground: var(--eclipse);
|
||||
|
||||
/* Ocean accent */
|
||||
--accent: oklch(0.450 0.150 230);
|
||||
--accent-foreground: var(--snow);
|
||||
|
||||
--accent-soft: oklch(0.920 0.040 230);
|
||||
--accent-soft-foreground: oklch(0.280 0.080 230);
|
||||
/* Form Field Defaults */
|
||||
--field-background: var(--white);
|
||||
--field-foreground: oklch(0.2103 0.0059 285.89);
|
||||
--field-placeholder: var(--muted);
|
||||
--field-border: transparent;
|
||||
|
||||
/* Status (kept compatible) */
|
||||
--success: oklch(0.5503 0.1244 153.56);
|
||||
--success-foreground: var(--snow);
|
||||
--success: oklch(0.7329 0.1935 150.81);
|
||||
--success-foreground: var(--eclipse);
|
||||
|
||||
--warning: oklch(0.7186 0.1521 64.85);
|
||||
--warning: oklch(0.7819 0.1585 72.33);
|
||||
--warning-foreground: var(--eclipse);
|
||||
|
||||
--danger: oklch(0.6259 0.1908 29.19);
|
||||
--danger: oklch(0.6532 0.2328 25.74);
|
||||
--danger-foreground: var(--snow);
|
||||
|
||||
/* Component Colors */
|
||||
@@ -113,14 +135,18 @@ Add a theme class to your HTML and apply colors to the body:
|
||||
|
||||
/* Misc */
|
||||
--border: oklch(0.50 0.060 230 / 22%);
|
||||
--divider: var(--color-neutral-200);
|
||||
--focus: oklch(0.60 0.150 230 / 35%);
|
||||
--divider: oklch(92% 0.004 286.32);
|
||||
--focus: var(--accent);
|
||||
--link: var(--accent);
|
||||
|
||||
--shadow-with-border:
|
||||
0 0px 5px 0px rgba(0, 10, 40, 0.03),
|
||||
0 2px 10px 0px rgba(0, 10, 40, 0.07),
|
||||
0 0px 1px 0px rgba(0, 0, 0, 0.25);
|
||||
/* Shadows */
|
||||
--surface-shadow:
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
|
||||
0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
--overlay-shadow: 0 4px 16px 0 rgba(24, 24, 27, 0.08), 0 8px 24px 0 rgba(24, 24, 27, 0.09);
|
||||
--field-shadow:
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
|
||||
0 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* Skeleton Default Global Animation */
|
||||
--skeleton-animation: shimmer; /* Possible values: shimmer, pulse, none */
|
||||
@@ -130,52 +156,56 @@ Add a theme class to your HTML and apply colors to the body:
|
||||
[data-theme="ocean-dark"] {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Base */
|
||||
/* Base Colors */
|
||||
--background: oklch(0.140 0.020 230);
|
||||
--foreground: var(--snow);
|
||||
--panel: oklch(0.180 0.020 230);
|
||||
|
||||
--muted: var(--color-neutral-400);
|
||||
--scrollbar: var(--color-neutral-800);
|
||||
/* Surface: Used for non-overlay components */
|
||||
--surface: oklch(0.2103 0.0059 285.89);
|
||||
--surface-foreground: var(--foreground);
|
||||
|
||||
--default: var(--color-neutral-900);
|
||||
--default-foreground: var(--color-neutral-200);
|
||||
/* Overlay: Used for floating/overlay components */
|
||||
--overlay: oklch(0.22 0.0059 285.89);
|
||||
--overlay-foreground: var(--foreground);
|
||||
|
||||
--surface-1: var(--background);
|
||||
--surface-2: oklch(0.220 0.030 230);
|
||||
--surface-3: oklch(0.280 0.030 230);
|
||||
--muted: oklch(70.5% 0.015 286.067);
|
||||
--scrollbar: oklch(70.5% 0.015 286.067);
|
||||
|
||||
--default: oklch(27.4% 0.006 286.033);
|
||||
--default-foreground: var(--snow);
|
||||
|
||||
/* Form Field Defaults */
|
||||
--field-background: var(--default);
|
||||
--field-foreground: var(--foreground);
|
||||
|
||||
/* Ocean accent */
|
||||
--accent: oklch(0.860 0.080 230);
|
||||
--accent-foreground: var(--eclipse);
|
||||
|
||||
--accent-soft: oklch(0.300 0.050 230);
|
||||
--accent-soft-foreground: var(--color-neutral-200);
|
||||
|
||||
/* Status */
|
||||
--success: oklch(80% 0.1561 154);
|
||||
--success: oklch(0.7329 0.1935 150.81);
|
||||
--success-foreground: var(--eclipse);
|
||||
|
||||
--warning: oklch(85% 0.1414 77.88);
|
||||
--warning: oklch(0.8203 0.1388 76.34);
|
||||
--warning-foreground: var(--eclipse);
|
||||
|
||||
--danger: oklch(0.5798 0.1635 30.83);
|
||||
--danger: oklch(0.594 0.1967 24.63);
|
||||
--danger-foreground: var(--snow);
|
||||
|
||||
/* Component Colors */
|
||||
--segment: oklch(1 0 0 / 16%);
|
||||
--segment: oklch(0.3964 0.01 285.93);
|
||||
--segment-foreground: var(--foreground);
|
||||
|
||||
/* Misc */
|
||||
--border: oklch(1 0 0 / 12%);
|
||||
--divider: var(--color-neutral-800);
|
||||
--focus: oklch(0.860 0.080 230 / 40%);
|
||||
--border: oklch(1 0 0 / 0%);
|
||||
--divider: oklch(22% 0.006 286.033);
|
||||
--focus: var(--accent);
|
||||
--link: var(--accent);
|
||||
|
||||
--shadow-with-border:
|
||||
0 0 0 1px var(--border),
|
||||
0 5px 10px -3px rgba(0, 10, 40, 0.35),
|
||||
0 3px 4px -2px rgba(0, 0, 0, 0.12);
|
||||
/* Shadows */
|
||||
--surface-shadow: 0 0 0 0 transparent inset;
|
||||
--overlay-shadow: 0 0 0 0 transparent inset;
|
||||
--field-shadow: 0 0 0 0 transparent inset;
|
||||
}
|
||||
}
|
||||
`}
|
||||
@@ -264,8 +294,8 @@ Import only what you need:
|
||||
/* Base requirements */
|
||||
@import "tailwindcss";
|
||||
@import "@heroui/styles/base/base.css" layer(base);
|
||||
@import "@heroui/styles/themes/theme.css" layer(theme);
|
||||
@import "@heroui/styles/themes/default.css" layer(theme);
|
||||
@import "@heroui/styles/themes/shared/theme.css" layer(theme);
|
||||
@import "@heroui/styles/themes/default" layer(theme);
|
||||
|
||||
/* Components (all components) */
|
||||
@import "@heroui/styles/components/index.css" layer(components);
|
||||
@@ -329,28 +359,27 @@ HeroUI defines three types of variables:
|
||||
|
||||
For a complete reference of all variables and their values, see:
|
||||
- [Colors Documentation](/docs/handbook/colors) - Visual color palette and base variables
|
||||
- [Default Theme Source](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/index.css) - Complete theme implementation
|
||||
- [Default Theme Variables](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/default/variables.css) - Complete theme variables
|
||||
- [Shared Theme Utilities](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/themes/shared/theme.css) - Calculated variables and utilities
|
||||
|
||||
### Calculated Variables (Tailwind)
|
||||
|
||||
We use Tailwind's `@theme` directive to automatically create calculated variables for hover states and radius variants:
|
||||
We use Tailwind's `@theme` directive to automatically create calculated variables for hover states and radius variants. These are defined in `themes/shared/theme.css`:
|
||||
<CollapsibleCode lang="css" code={` @theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-panel: var(--panel);
|
||||
--color-panel-foreground: var(--panel-foreground);
|
||||
|
||||
--color-surface: var(--surface);
|
||||
--color-surface-foreground: var(--surface-foreground);
|
||||
|
||||
--color-overlay: var(--overlay);
|
||||
--color-overlay-foreground: var(--overlay-foreground);
|
||||
|
||||
--color-muted: var(--muted);
|
||||
|
||||
--color-surface: var(--surface-1);
|
||||
--color-surface-foreground: var(--foreground);
|
||||
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
|
||||
--color-accent-soft: var(--accent-soft);
|
||||
--color-accent-soft-foreground: var(--accent-soft-foreground);
|
||||
|
||||
--color-segment: var(--segment);
|
||||
--color-segment-foreground: var(--segment-foreground);
|
||||
|
||||
@@ -361,10 +390,6 @@ We use Tailwind's `@theme` directive to automatically create calculated variable
|
||||
|
||||
--color-default: var(--default);
|
||||
--color-default-foreground: var(--default-foreground);
|
||||
--color-field: var(--field-background, var(--color-default));
|
||||
--color-field-foreground: var(--field-foreground, var(--color-foreground));
|
||||
--color-field-placeholder: var(--field-placeholder, var(--color-muted));
|
||||
--color-field-border: var(--field-border, var(--color-border));
|
||||
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
@@ -375,43 +400,49 @@ We use Tailwind's `@theme` directive to automatically create calculated variable
|
||||
--color-danger: var(--danger);
|
||||
--color-danger-foreground: var(--danger-foreground);
|
||||
|
||||
--shadow-panel: var(--panel-shadow);
|
||||
--shadow-surface: var(--surface-shadow);
|
||||
--shadow-overlay: var(--overlay-shadow);
|
||||
--shadow-field: var(--field-shadow);
|
||||
|
||||
/* Form Field Tokens */
|
||||
--color-field: var(--field-background, var(--color-default));
|
||||
--color-field-hover: color-mix(
|
||||
in oklab,
|
||||
var(--field-background, var(--color-default)) 90%,
|
||||
var(--field-foreground, var(--color-default-foreground)) 10%
|
||||
);
|
||||
--color-field-foreground: var(--field-foreground, var(--color-foreground));
|
||||
--color-field-placeholder: var(--field-placeholder, var(--color-muted));
|
||||
--color-field-border: var(--field-border, var(--color-border));
|
||||
--radius-field: var(--field-radius, var(--radius-xl));
|
||||
--border-width-field: var(--field-border-width, var(--border-width));
|
||||
|
||||
/* Calculated Variables */
|
||||
|
||||
/* Colors */
|
||||
--color-panel-hover: color-mix(
|
||||
/* --- background shades --- */
|
||||
--color-background-secondary: color-mix(
|
||||
in oklab,
|
||||
var(--color-panel) 90%,
|
||||
var(--color-panel-foreground) 5%
|
||||
var(--color-background) 96%,
|
||||
var(--color-foreground) 4%
|
||||
);
|
||||
--color-default-hover: color-mix(
|
||||
--color-background-tertiary: color-mix(
|
||||
in oklab,
|
||||
var(--color-default) 90%,
|
||||
var(--color-default-foreground) 2%
|
||||
var(--color-background) 92%,
|
||||
var(--color-foreground) 8%
|
||||
);
|
||||
--color-field-hover: color-mix(
|
||||
--color-background-quaternary: color-mix(
|
||||
in oklab,
|
||||
var(--color-field) 88%,
|
||||
var(--color-field-foreground) 8%
|
||||
);
|
||||
--color-field-focus: color-mix(
|
||||
in oklab,
|
||||
var(--color-field) 80%,
|
||||
var(--color-field-foreground) 14%
|
||||
var(--color-background) 86%,
|
||||
var(--color-foreground) 14%
|
||||
);
|
||||
--color-background-inverse: var(--color-foreground);
|
||||
|
||||
/* Hover states */
|
||||
--color-default-hover: color-mix(in oklab, var(--color-default) 80%, transparent);
|
||||
--color-accent-hover: color-mix(
|
||||
in oklab,
|
||||
var(--color-accent) 90%,
|
||||
var(--color-accent-foreground) 20%
|
||||
);
|
||||
--color-accent-soft-hover: color-mix(
|
||||
in oklab,
|
||||
var(--color-accent-soft) 10%,
|
||||
var(--color-accent-soft-foreground) 18%
|
||||
var(--color-accent-foreground) 10%
|
||||
);
|
||||
--color-success-hover: color-mix(
|
||||
in oklab,
|
||||
@@ -428,10 +459,18 @@ We use Tailwind's `@theme` directive to automatically create calculated variable
|
||||
var(--color-danger) 90%,
|
||||
var(--color-danger-foreground) 10%
|
||||
);
|
||||
|
||||
/* Form Field Colors */
|
||||
--color-field-hover: color-mix(
|
||||
in oklab,
|
||||
var(--color-field) 90%,
|
||||
var(--color-field-foreground) 2%
|
||||
);
|
||||
--color-field-focus: var(--color-field);
|
||||
--color-field-border-hover: color-mix(
|
||||
in oklab,
|
||||
var(--color-field-border) 88%,
|
||||
var(--color-field-foreground) 8%
|
||||
var(--color-field-foreground) 10%
|
||||
);
|
||||
--color-field-border-focus: color-mix(
|
||||
in oklab,
|
||||
@@ -439,75 +478,101 @@ We use Tailwind's `@theme` directive to automatically create calculated variable
|
||||
var(--color-field-foreground) 22%
|
||||
);
|
||||
|
||||
/* Surface Colors */
|
||||
--color-surface-1: var(--surface-1);
|
||||
--color-surface-1-foreground: color-mix(
|
||||
/* Soft Colors */
|
||||
--color-accent-soft: color-mix(in oklab, var(--color-accent) 15%, transparent);
|
||||
--color-accent-soft-foreground: var(--color-accent);
|
||||
--color-accent-soft-hover: color-mix(in oklab, var(--color-accent) 20%, transparent);
|
||||
|
||||
--color-danger-soft: color-mix(in oklab, var(--color-danger) 15%, transparent);
|
||||
--color-danger-soft-foreground: var(--color-danger);
|
||||
--color-danger-soft-hover: color-mix(in oklab, var(--color-danger) 20%, transparent);
|
||||
|
||||
--color-warning-soft: color-mix(in oklab, var(--color-warning) 15%, transparent);
|
||||
--color-warning-soft-foreground: var(--color-warning);
|
||||
--color-warning-soft-hover: color-mix(in oklab, var(--color-warning) 20%, transparent);
|
||||
|
||||
--color-success-soft: color-mix(in oklab, var(--color-success) 15%, transparent);
|
||||
--color-success-soft-foreground: var(--color-success);
|
||||
--color-success-soft-hover: color-mix(in oklab, var(--color-success) 20%, transparent);
|
||||
|
||||
/* Surface Levels */
|
||||
--color-surface-secondary: color-mix(in oklab, var(--surface) 94%, var(--surface-foreground) 6%);
|
||||
--color-surface-tertiary: color-mix(in oklab, var(--surface) 92%, var(--surface-foreground) 8%);
|
||||
--color-surface-quaternary: color-mix(
|
||||
in oklab,
|
||||
var(--color-surface-1) 80%,
|
||||
var(--color-foreground) 3.5%
|
||||
);
|
||||
--color-surface-2: var(--surface-2);
|
||||
--color-surface-2-foreground: color-mix(
|
||||
in oklab,
|
||||
var(--color-surface-2) 80%,
|
||||
var(--color-foreground) 3.5%
|
||||
);
|
||||
--color-surface-3: var(--surface-3);
|
||||
--color-surface-3-foreground: color-mix(
|
||||
in oklab,
|
||||
var(--color-surface-3) 80%,
|
||||
var(--color-foreground) 3.5%
|
||||
var(--surface) 86%,
|
||||
var(--default-foreground) 14%
|
||||
);
|
||||
|
||||
/* Radius */
|
||||
--radius-xs: calc(var(--radius) * 0.25);
|
||||
--radius-sm: calc(var(--radius) * 0.5);
|
||||
--radius-md: calc(var(--radius) * 0.75);
|
||||
--radius-lg: calc(var(--radius) * 1);
|
||||
--radius-xl: calc(var(--radius) * 1.5);
|
||||
--radius-2xl: calc(var(--radius) * 2);
|
||||
--radius-3xl: calc(var(--radius) * 3);
|
||||
--radius-4xl: calc(var(--radius) * 4);
|
||||
--radius-panel: var(--radius-xl);
|
||||
--radius-panel-inner: var(--radius-2xl);
|
||||
/* On Surface Colors */
|
||||
--color-on-surface: color-mix(
|
||||
in oklab,
|
||||
var(--color-surface) 93%,
|
||||
var(--color-surface-foreground) 7%
|
||||
);
|
||||
--color-on-surface-foreground: var(--color-surface-foreground);
|
||||
--color-on-surface-hover: color-mix(
|
||||
in oklab,
|
||||
var(--color-surface) 91%,
|
||||
var(--color-surface-foreground) 9%
|
||||
);
|
||||
--color-on-surface-focus: var(--color-on-surface);
|
||||
|
||||
/* Radius scale */
|
||||
--radius-xs: calc(var(--radius) * 0.25); /* 0.125rem (2px) */
|
||||
--radius-sm: calc(var(--radius) * 0.5); /* 0.25rem (4px) */
|
||||
--radius-md: calc(var(--radius) * 0.75); /* 0.375rem (6px) */
|
||||
--radius-lg: calc(var(--radius) * 1); /* 0.5rem (8px) */
|
||||
--radius-xl: calc(var(--radius) * 1.5); /* 0.75rem (12px) */
|
||||
--radius-2xl: calc(var(--radius) * 2); /* 1rem (16px) */
|
||||
--radius-3xl: calc(var(--radius) * 3); /* 1.5rem (24px) */
|
||||
--radius-4xl: calc(var(--radius) * 4); /* 2rem (32px) */
|
||||
|
||||
/* Transition Timing Functions */
|
||||
--ease-smooth: ease; /* same as transition: ease; */
|
||||
/* These custom curves are made by https://twitter.com/bdc */
|
||||
|
||||
/* From smoother to faster */
|
||||
--ease-smooth: ease;
|
||||
--ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
|
||||
--ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
--ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22);
|
||||
--ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||
--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
|
||||
--ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335);
|
||||
/* From slower to faster */
|
||||
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
--ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
|
||||
--ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
/* Custom smooth-out curve: fast start, smooth stop - Apple style */
|
||||
--ease-fluid-out: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
/* From slower to faster */
|
||||
--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
--ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1);
|
||||
--ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
|
||||
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
|
||||
--ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||
--ease-linear: linear;
|
||||
|
||||
/* Animations */
|
||||
--animate-spin-fast: spin 0.75s linear infinite;
|
||||
--animate-skeleton: skeleton 2s linear infinite;
|
||||
--animate-caret-blink: caret-blink 1.2s ease-out infinite;
|
||||
|
||||
@keyframes skeleton {
|
||||
100% {
|
||||
transform: translateX(200%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes caret-blink {
|
||||
0%,
|
||||
70%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
20%,
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -8,9 +8,10 @@ icon: book-open
|
||||
HeroUI is a UI library for React that helps you build beautiful and accessible user interfaces. Created on top of [Tailwind CSS v4](https://tailwindcss.com/) and [React Aria Components](https://react-spectrum.adobe.com/react-aria/index.html).
|
||||
|
||||
<DocsImage
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/docs-intro-cover.jpg"
|
||||
darkSrc="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/docs-intro-cover-dark.jpg"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/heroui-og_2x.jpg"
|
||||
darkSrc="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/heroui-og-black_2x.jpg"
|
||||
alt="HeroUI v3 Introduction"
|
||||
className="h-[220px] md:h-[384px]"
|
||||
/>
|
||||
|
||||
Every component is designed with care: smooth animations, polished details, and accessibility built in. It works out of the box, yet gives you the freedom to make it your own.
|
||||
@@ -101,7 +102,7 @@ Huge thanks to [Devon Govett](https://x.com/devongovett) and the Adobe team for
|
||||
Yes, completely free and open source under the MIT license.
|
||||
|
||||
### Is it production-ready?
|
||||
Currently in alpha. We're actively working towards a stable release with community feedback.
|
||||
Currently in **beta**. We're actively working towards a stable release with community feedback.
|
||||
|
||||
### Can I customize the components?
|
||||
Yes! Use Tailwind utilities, CSS variables, BEM modifiers, or compose component parts differently. Every slot is customizable.
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
"ui-for-agents/mcp-server",
|
||||
"---Components---",
|
||||
"components/accordion",
|
||||
"components/alert",
|
||||
"components/avatar",
|
||||
"components/button",
|
||||
"components/card",
|
||||
"components/checkbox",
|
||||
"components/checkbox-group",
|
||||
"components/chip",
|
||||
"components/close-button",
|
||||
"components/description",
|
||||
@@ -36,15 +38,20 @@
|
||||
"components/fieldset",
|
||||
"components/form",
|
||||
"components/input",
|
||||
"components/input-otp",
|
||||
"components/kbd",
|
||||
"components/label",
|
||||
"components/link",
|
||||
"components/listbox",
|
||||
"components/popover",
|
||||
"components/radio-group",
|
||||
"components/separator",
|
||||
"components/select",
|
||||
"components/skeleton",
|
||||
"components/slider",
|
||||
"components/spinner",
|
||||
"components/switch",
|
||||
"components/surface",
|
||||
"components/tabs",
|
||||
"components/text-field",
|
||||
"components/textarea",
|
||||
|
||||
@@ -16,22 +16,22 @@ Install HeroUI and required dependencies:
|
||||
<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
|
||||
<Tab value="npm">
|
||||
```bash
|
||||
npm i @heroui/styles@alpha @heroui/react@alpha
|
||||
npm i @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="pnpm">
|
||||
```bash
|
||||
pnpm add @heroui/styles@alpha @heroui/react@alpha
|
||||
pnpm add @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="yarn">
|
||||
```bash
|
||||
yarn add @heroui/styles@alpha @heroui/react@alpha
|
||||
yarn add @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="bun">
|
||||
```bash
|
||||
bun add @heroui/styles@alpha @heroui/react@alpha
|
||||
bun add @heroui/styles@beta @heroui/react@beta
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
---
|
||||
title: MCP Server
|
||||
description: Access HeroUI v3 documentation directly in your AI assistant
|
||||
icon: preview
|
||||
---
|
||||
|
||||
The HeroUI MCP Server gives AI assistants direct access to HeroUI v3 component documentation, making it easier to build with HeroUI in AI-powered development environments.
|
||||
|
||||
<DocsImage
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/hero-react-mcp-light-3.jpg"
|
||||
darkSrc="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/hero-react-mcp-dark-2.jpg"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/hero-react-mcp-light-new-2.jpg"
|
||||
darkSrc="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/hero-react-mcp-dark-new.jpg"
|
||||
alt="HeroUI v3 MCP Server"
|
||||
/>
|
||||
|
||||
@@ -26,12 +25,12 @@ The MCP server currently supports **@heroui/react v3** only and [stdio transport
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<a
|
||||
href="https://link.heroui.com/mcp-cursor-install"
|
||||
className="button button--tertiary button--sm no-underline bg-default/50 dark:bg-default/30"
|
||||
className="button button--tertiary button--sm no-underline"
|
||||
>
|
||||
<svg viewBox="0 0 466.73 532.09" className="w-5 h-5 fill-current">
|
||||
<path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"/>
|
||||
</svg>
|
||||
<span className="text-accent">Install in Cursor</span>
|
||||
<span >Install in Cursor</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,19 @@ const config: NextConfig = {
|
||||
optimizePackageImports: ["@heroui/react"],
|
||||
reactCompiler: true,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
headers: [
|
||||
{
|
||||
key: "X-Robots-Tag",
|
||||
value: "index, follow",
|
||||
},
|
||||
],
|
||||
source: "/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@heroui/react": "workspace:*",
|
||||
"@iconify-json/gravity-ui": "1.2.10",
|
||||
"@iconify/react": "6.0.2",
|
||||
"@react-stately/data": "3.14.1",
|
||||
"@t3-oss/env-nextjs": "0.13.8",
|
||||
"@vercel/analytics": "1.5.0",
|
||||
"@vercel/og": "0.8.5",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 22 KiB |
@@ -6,4 +6,4 @@ Allow: /llms.txt
|
||||
Allow: /llms-full.txt
|
||||
|
||||
# Sitemap
|
||||
Sitemap: https://heroui.dev/sitemap.xml
|
||||
Sitemap: https://v3.heroui.com/sitemap.xml
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Alert, Button} from "@heroui/react";
|
||||
|
||||
export function AlertDemo() {
|
||||
return (
|
||||
<Alert className="w-[400px] items-center">
|
||||
<Alert.Indicator />
|
||||
<Alert.Content>
|
||||
<Alert.Title>You have 2 credits left</Alert.Title>
|
||||
<Alert.Description>Get a paid plan for more credits</Alert.Description>
|
||||
</Alert.Content>
|
||||
<Button variant="tertiary">Upgrade</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {Avatar, Button, Card, CloseButton} from "@heroui/react";
|
||||
|
||||
import {Iconify} from "@/components/iconify";
|
||||
|
||||
export function AlertDialogDemo() {
|
||||
return (
|
||||
<Card className="h-[208px] w-[340px] items-start justify-center p-5">
|
||||
<Card.Header className="flex w-full items-start justify-center gap-2 px-1">
|
||||
<Avatar color="warning" variant="soft">
|
||||
<Avatar.Fallback>
|
||||
<Iconify className="text-lg" icon="gravity-ui:floppy-disk" />
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<Card.Title>Unsaved changes</Card.Title>
|
||||
<Card.Description>Do you want to save or discard changes?</Card.Description>
|
||||
<CloseButton className="absolute right-3 top-3" />
|
||||
</Card.Header>
|
||||
<Card.Footer className="flex w-full items-center gap-2 px-0.5 pt-3">
|
||||
<Button className="w-full" variant="tertiary">
|
||||
Discard
|
||||
</Button>
|
||||
<Button className="w-full">Save changes</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {Description, Label, Switch} from "@heroui/react";
|
||||
|
||||
export function AllowNotificationsDemo() {
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<Switch defaultSelected>
|
||||
<div className="flex gap-8 p-4">
|
||||
<div className="-mt-0.5 flex flex-col justify-start gap-1">
|
||||
<Label className="w-fit text-sm font-medium">Allow notifications</Label>
|
||||
<Description>Receive push notifications from HeroUI</Description>
|
||||
</div>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</div>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {Avatar} from "@heroui/react";
|
||||
import Image from "next/image";
|
||||
|
||||
const avatars = [
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
|
||||
name: "Blue",
|
||||
},
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg",
|
||||
name: "Green",
|
||||
},
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg",
|
||||
name: "Purple",
|
||||
},
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg",
|
||||
name: "Orange",
|
||||
},
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg",
|
||||
name: "red",
|
||||
},
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
|
||||
name: "Blue",
|
||||
},
|
||||
{
|
||||
image: "https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/black.jpg",
|
||||
name: "Black",
|
||||
},
|
||||
];
|
||||
|
||||
export function AvatarGroupDemo() {
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="flex -space-x-2">
|
||||
{avatars.slice(0, 5).map((item, index) => (
|
||||
<Avatar key={`${item.name}-${index}`} className="ring-background ring-2">
|
||||
<Avatar.Image asChild alt={item.name}>
|
||||
<Image alt={item.name} height={40} src={item.image} width={40} />
|
||||
</Avatar.Image>
|
||||
<Avatar.Fallback>
|
||||
{item.name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
))}
|
||||
<Avatar className="ring-background ring-2">
|
||||
<Avatar.Fallback className="bg-surface text-muted text-xs font-medium">
|
||||
+{avatars.length - 2}
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {Button} from "@heroui/react";
|
||||
|
||||
export const ButtonsDemo = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button size="sm">Call to action</Button>
|
||||
<Button size="sm" variant="secondary">
|
||||
Call to action
|
||||
</Button>
|
||||
<Button size="sm" variant="tertiary">
|
||||
Call to action
|
||||
</Button>
|
||||
<Button size="sm" variant="danger">
|
||||
Call to action
|
||||
</Button>
|
||||
<Button size="sm" variant="danger-soft">
|
||||
Call to action
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost">
|
||||
Call to action
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {AlertDemo} from "./alert-demo";
|
||||
import {AlertDialogDemo} from "./alert-dialog-demo";
|
||||
import {AllowNotificationsDemo} from "./allow-notifications-demo";
|
||||
import {AvatarGroupDemo} from "./avatar-group-demo";
|
||||
import {ButtonsDemo} from "./buttons-demo";
|
||||
import {InputOTPDemo} from "./input-otp-demo";
|
||||
import {ListBoxDemo} from "./listbox-demo";
|
||||
import {LoginDemo} from "./login-demo";
|
||||
import {SelectDemo} from "./select-demo";
|
||||
import {SliderDemo} from "./slider-demo";
|
||||
import {SubtleCardsDemo} from "./subtle-cards-demo";
|
||||
import {TabsDemo1} from "./tabs-1-demo";
|
||||
import {TabsDemo2} from "./tabs-2-demo";
|
||||
import {TextfieldDemo} from "./textfield-demo";
|
||||
import {UIComponentsDemo} from "./ui-components-demo";
|
||||
import {XProfileDemo} from "./x-profile-demo";
|
||||
|
||||
export function DemoComponents() {
|
||||
return (
|
||||
<div className="mx-auto grid grid-cols-3 gap-8 py-24">
|
||||
{/* Left */}
|
||||
<div className="flex flex-col items-center gap-10">
|
||||
<TextfieldDemo />
|
||||
<SelectDemo />
|
||||
<UIComponentsDemo />
|
||||
<SliderDemo />
|
||||
<TabsDemo1 />
|
||||
<TabsDemo2 />
|
||||
<ListBoxDemo />
|
||||
</div>
|
||||
{/* Center */}
|
||||
<div className="flex flex-col items-center gap-10">
|
||||
<AvatarGroupDemo />
|
||||
<InputOTPDemo />
|
||||
<ButtonsDemo />
|
||||
<XProfileDemo />
|
||||
<AlertDemo />
|
||||
<AllowNotificationsDemo />
|
||||
</div>
|
||||
{/* Right */}
|
||||
<div className="flex flex-col items-center gap-10">
|
||||
<LoginDemo />
|
||||
<SubtleCardsDemo />
|
||||
<AlertDialogDemo />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import {InputOTP, Label, Link} from "@heroui/react";
|
||||
import {useState} from "react";
|
||||
|
||||
export function InputOTPDemo() {
|
||||
// HERO - 4320 - easter egg
|
||||
const [value, setValue] = useState("4320");
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="flex w-[280px] flex-col items-start justify-center gap-2">
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<Label>Verify account</Label>
|
||||
<p className="text-muted text-sm">We've sent a code to a****@gmail.com</p>
|
||||
</div>
|
||||
<InputOTP maxLength={6} value={value} onChange={setValue}>
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={0} />
|
||||
<InputOTP.Slot index={1} />
|
||||
<InputOTP.Slot index={2} />
|
||||
</InputOTP.Group>
|
||||
<InputOTP.Separator />
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={3} />
|
||||
<InputOTP.Slot index={4} />
|
||||
<InputOTP.Slot index={5} />
|
||||
</InputOTP.Group>
|
||||
</InputOTP>
|
||||
<div className="flex items-center gap-[5px] px-1 pt-1">
|
||||
<p className="text-muted text-sm">Didn't receive a code?</p>
|
||||
<Link className="text-foreground" underline="always">
|
||||
Resend
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import {Description, Header, Kbd, Label, ListBox, Separator, Surface} from "@heroui/react";
|
||||
|
||||
import {Iconify} from "@/components/iconify";
|
||||
|
||||
export function ListBoxDemo() {
|
||||
return (
|
||||
<Surface className="shadow-surface w-[256px] rounded-3xl">
|
||||
<ListBox aria-label="File actions" className="w-full p-2" selectionMode="none">
|
||||
<ListBox.Section>
|
||||
<Header>Actions</Header>
|
||||
<ListBox.Item id="new-file" textValue="New file">
|
||||
<div className="flex h-8 items-start justify-center pt-px">
|
||||
<Iconify className="text-muted size-4 flex-shrink-0" icon="gravity-ui:square-plus" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Label>New file</Label>
|
||||
<Description>Create a new file</Description>
|
||||
</div>
|
||||
<Kbd className="ms-auto" variant="light">
|
||||
<Kbd.Abbr keyValue="command" />
|
||||
<Kbd.Content>N</Kbd.Content>
|
||||
</Kbd>
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="edit-file" textValue="Edit file">
|
||||
<div className="flex h-8 items-start justify-center pt-px">
|
||||
<Iconify className="text-muted size-4 flex-shrink-0" icon="gravity-ui:pencil" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Label>Edit file</Label>
|
||||
<Description>Make changes</Description>
|
||||
</div>
|
||||
<Kbd className="ms-auto" variant="light">
|
||||
<Kbd.Abbr keyValue="command" />
|
||||
<Kbd.Content>E</Kbd.Content>
|
||||
</Kbd>
|
||||
</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
<Separator />
|
||||
<ListBox.Section>
|
||||
<Header>Danger zone</Header>
|
||||
<ListBox.Item id="delete-file" textValue="Delete file" variant="danger">
|
||||
<div className="flex h-8 items-start justify-center pt-px">
|
||||
<Iconify className="text-danger size-4 flex-shrink-0" icon="gravity-ui:trash-bin" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Label>Delete file</Label>
|
||||
<Description>Move to trash</Description>
|
||||
</div>
|
||||
<Kbd className="ms-auto" variant="light">
|
||||
<Kbd.Abbr keyValue="command" />
|
||||
<Kbd.Abbr keyValue="shift" />
|
||||
<Kbd.Content>D</Kbd.Content>
|
||||
</Kbd>
|
||||
</ListBox.Item>
|
||||
</ListBox.Section>
|
||||
</ListBox>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {Avatar, Button, Card, CloseButton, Separator} from "@heroui/react";
|
||||
|
||||
import {Iconify} from "@/components/iconify";
|
||||
import {AppleIcon} from "@/icons/apple";
|
||||
import {GoogleIcon} from "@/icons/google";
|
||||
|
||||
export function LoginDemo() {
|
||||
return (
|
||||
<Card className="w-[320px] items-start justify-center p-5">
|
||||
<Card.Header className="flex w-full items-center justify-center gap-2">
|
||||
<Avatar>
|
||||
<Avatar.Fallback>
|
||||
<Iconify icon="gravity-ui:person" />
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<Card.Title>Create an account</Card.Title>
|
||||
<CloseButton className="absolute right-3 top-3" />
|
||||
</Card.Header>
|
||||
<Card.Content className="gap-2">
|
||||
<p className="text-muted text-center text-sm font-medium">
|
||||
Start your free 7-day trial. No credit card required.
|
||||
</p>
|
||||
<Button className="w-full">Get Started</Button>
|
||||
<div className="flex w-full items-center gap-2 py-2">
|
||||
<Separator className="flex-1" />
|
||||
<p className="text-muted text-center text-xs font-medium uppercase">Or</p>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
<Button className="w-full" variant="tertiary">
|
||||
<GoogleIcon />
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button className="w-full" variant="tertiary">
|
||||
<AppleIcon />
|
||||
Continue with Apple
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {Label, ListBox, Select} from "@heroui/react";
|
||||
|
||||
export function SelectDemo() {
|
||||
return (
|
||||
<Select isRequired className="w-[256px]" placeholder="Select one">
|
||||
<Label>State</Label>
|
||||
<Select.Trigger>
|
||||
<Select.Value />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<ListBox>
|
||||
<ListBox.Item id="florida" textValue="Florida">
|
||||
Florida
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="delaware" textValue="Delaware">
|
||||
Delaware
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="california" textValue="California">
|
||||
California
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="texas" textValue="Texas">
|
||||
Texas
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="new-york" textValue="New York">
|
||||
New York
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
<ListBox.Item id="washington" textValue="Washington">
|
||||
Washington
|
||||
<ListBox.ItemIndicator />
|
||||
</ListBox.Item>
|
||||
</ListBox>
|
||||
</Select.Content>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import {Label, Slider} from "@heroui/react";
|
||||
|
||||
export function SliderDemo() {
|
||||
return (
|
||||
<div className="w-[256px] px-1">
|
||||
<Slider
|
||||
className="w-full max-w-xs"
|
||||
defaultValue={250}
|
||||
formatOptions={{currency: "USD", style: "currency"}}
|
||||
maxValue={500}
|
||||
minValue={0}
|
||||
step={10}
|
||||
>
|
||||
<Label>Price</Label>
|
||||
<Slider.Output />
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
<Slider.Thumb />
|
||||
</Slider.Track>
|
||||
</Slider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {Avatar, Card} from "@heroui/react";
|
||||
import Image from "next/image";
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
const cardStyles = tv({
|
||||
slots: {
|
||||
avatar: "size-[56px] rounded-xl",
|
||||
card: "w-full",
|
||||
cardContent: "items-start",
|
||||
footer: "items-center gap-2",
|
||||
footerAvatar: "size-4",
|
||||
},
|
||||
});
|
||||
|
||||
export function SubtleCardsDemo() {
|
||||
const {avatar, card, cardContent, footer, footerAvatar} = cardStyles();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-row gap-4">
|
||||
<Card className={card()}>
|
||||
<Card.Header>
|
||||
<Avatar className={avatar()}>
|
||||
<Avatar.Image asChild>
|
||||
<Image
|
||||
alt="Demo 1"
|
||||
height={56}
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/demo1.jpg"
|
||||
width={56}
|
||||
/>
|
||||
</Avatar.Image>
|
||||
<Avatar.Fallback>JK</Avatar.Fallback>
|
||||
</Avatar>
|
||||
</Card.Header>
|
||||
<Card.Content className={cardContent()}>
|
||||
<p className="text-sm font-medium">Indie Hackers</p>
|
||||
<p className="text-muted text-sm">148 members</p>
|
||||
</Card.Content>
|
||||
<Card.Footer className={footer()}>
|
||||
<Avatar className={footerAvatar()}>
|
||||
<Avatar.Image asChild>
|
||||
<Image
|
||||
alt="John"
|
||||
height={16}
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg"
|
||||
width={16}
|
||||
/>
|
||||
</Avatar.Image>
|
||||
<Avatar.Fallback>JK</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<p className="text-muted text-xs">By John</p>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
<Card className={card()}>
|
||||
<Card.Header>
|
||||
<Avatar className={avatar()}>
|
||||
<Avatar.Image asChild>
|
||||
<Image
|
||||
alt="AI Builders"
|
||||
height={56}
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/demo2.jpg"
|
||||
width={56}
|
||||
/>
|
||||
</Avatar.Image>
|
||||
<Avatar.Fallback>J</Avatar.Fallback>
|
||||
</Avatar>
|
||||
</Card.Header>
|
||||
<Card.Content className={cardContent()}>
|
||||
<p className="text-sm font-medium">AI Builders</p>
|
||||
<p className="text-muted text-sm">362 members</p>
|
||||
</Card.Content>
|
||||
<Card.Footer className={footer()}>
|
||||
<Avatar className={footerAvatar()}>
|
||||
<Avatar.Image asChild>
|
||||
<Image
|
||||
alt="Martha"
|
||||
height={16}
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg"
|
||||
width={16}
|
||||
/>
|
||||
</Avatar.Image>
|
||||
<Avatar.Fallback>M</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<p className="text-muted text-xs">By Martha</p>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {Tabs} from "@heroui/react";
|
||||
|
||||
export function TabsDemo1() {
|
||||
return (
|
||||
<Tabs className="w-[256px]">
|
||||
<Tabs.ListContainer>
|
||||
<Tabs.List aria-label="Options">
|
||||
<Tabs.Tab id="1d">
|
||||
1D
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab id="7d">
|
||||
7D
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab id="1m">
|
||||
1M
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab id="1y">
|
||||
1Y
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab id="all">
|
||||
All
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListContainer>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {Tabs} from "@heroui/react";
|
||||
|
||||
import {Iconify} from "@/components/iconify";
|
||||
|
||||
export function TabsDemo2() {
|
||||
return (
|
||||
<Tabs className="w-[256px]">
|
||||
<Tabs.ListContainer>
|
||||
<Tabs.List aria-label="Options">
|
||||
<Tabs.Tab className="gap-1.5" id="chats">
|
||||
<Iconify icon="gravity-ui:comment" />
|
||||
<span>Chats</span>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab className="gap-1.5" id="emails">
|
||||
<Iconify icon="gravity-ui:envelope" />
|
||||
<span>Emails</span>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs.ListContainer>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Description, FieldError, Input, Label, TextField} from "@heroui/react";
|
||||
|
||||
export function TextfieldDemo() {
|
||||
return (
|
||||
<div>
|
||||
<TextField isRequired className="items-start" name="name">
|
||||
<Label>Your email</Label>
|
||||
<Input className="w-[256px]" placeholder="john@email.com" />
|
||||
<Description className="mt-0.5">We'll never share this with anyone else</Description>
|
||||
<FieldError>The email is invalid</FieldError>
|
||||
</TextField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {Checkbox, Radio, RadioGroup, Spinner, Switch} from "@heroui/react";
|
||||
|
||||
export function UIComponentsDemo() {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center gap-8">
|
||||
{/* Checkbox - Selected State */}
|
||||
<Checkbox defaultSelected>
|
||||
<Checkbox.Control>
|
||||
<Checkbox.Indicator />
|
||||
</Checkbox.Control>
|
||||
</Checkbox>
|
||||
|
||||
{/* Switch - On State */}
|
||||
<Switch defaultSelected>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
|
||||
{/* Radio Buttons - Unselected and Selected */}
|
||||
<RadioGroup className="gap-8" defaultValue="option2" name="demo" orientation="horizontal">
|
||||
<Radio value="option1">
|
||||
<Radio.Control>
|
||||
<Radio.Indicator />
|
||||
</Radio.Control>
|
||||
</Radio>
|
||||
<Radio value="option2">
|
||||
<Radio.Control>
|
||||
<Radio.Indicator />
|
||||
</Radio.Control>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
{/* Spinner */}
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {Avatar, Card} from "@heroui/react";
|
||||
import Image from "next/image";
|
||||
|
||||
import {VerifiedBadgeIcon} from "@/icons/verified-badge";
|
||||
|
||||
export function XProfileDemo() {
|
||||
return (
|
||||
<Card className="w-[400px] items-start justify-center">
|
||||
<Card.Header className="items-top w-full flex-row justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar size="sm">
|
||||
<Avatar.Image asChild alt="HeroUI">
|
||||
<Image
|
||||
alt="HeroUI"
|
||||
height={32}
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/heroui_isotipo.png"
|
||||
width={32}
|
||||
/>
|
||||
</Avatar.Image>
|
||||
<Avatar.Fallback>H</Avatar.Fallback>
|
||||
</Avatar>
|
||||
<div className="flex h-full flex-col items-start justify-center">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-sm font-semibold leading-4">HeroUI</span>
|
||||
<VerifiedBadgeIcon height={18} width={18} />
|
||||
</div>
|
||||
<span className="text-muted text-sm tracking-tight">@hero_ui</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content className="flex-row text-left">
|
||||
<p className="pl-px text-sm font-medium">
|
||||
Building the future of UI for web & mobile.
|
||||
<br />
|
||||
<span aria-label="confetti" role="img">
|
||||
🚀
|
||||
</span>
|
||||
(YC S24)
|
||||
</p>
|
||||
</Card.Content>
|
||||
<Card.Footer className="gap-2">
|
||||
<div className="flex gap-1">
|
||||
<p className="text-sm font-semibold">4</p>
|
||||
<p className="text-muted text-sm">Following</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<p className="text-sm font-semibold">97.1K</p>
|
||||
<p className="text-muted text-sm">Followers</p>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -89,36 +89,11 @@ export default function Layout({children}: {children: ReactNode}) {
|
||||
// text: "Theming",
|
||||
// url: "/docs/theming",
|
||||
// },
|
||||
{
|
||||
external: true,
|
||||
icon: (
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
),
|
||||
text: "Github",
|
||||
type: "icon",
|
||||
url: "https://github.com/heroui-inc/heroui",
|
||||
},
|
||||
]}
|
||||
// links={[
|
||||
// {
|
||||
// children: (
|
||||
// <NavbarMenu>
|
||||
// <NavbarMenuTrigger>Documentation</NavbarMenuTrigger>
|
||||
// <NavbarMenuContent>
|
||||
// <NavbarMenuLink href="/docs">Hello World</NavbarMenuLink>
|
||||
// </NavbarMenuContent>
|
||||
// </NavbarMenu>
|
||||
// ),
|
||||
// // only displayed on navbar, not mobile menu
|
||||
// on: "nav",
|
||||
// type: "custom",
|
||||
// },
|
||||
// other items
|
||||
// ]}
|
||||
themeSwitch={{
|
||||
mode: "light-dark-system",
|
||||
}}
|
||||
>
|
||||
<div aria-hidden="true" className="gradient-background home-gradient-background" />
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import {buttonVariants} from "@heroui/react";
|
||||
import LinkRoot from "fumadocs-core/link";
|
||||
import Image from "next/image";
|
||||
|
||||
// TODO:
|
||||
import {GitHubLink} from "@/components/github-link";
|
||||
import {Iconify} from "@/components/iconify";
|
||||
import {SocialLinks} from "@/components/social-links";
|
||||
// import {ThemeBackground} from "@/components/theme-background";
|
||||
import {currentVersion} from "@/utils/version";
|
||||
|
||||
import {DemoComponents} from "./components/demo";
|
||||
|
||||
const VersionChip = () => {
|
||||
return (
|
||||
<LinkRoot
|
||||
className="chip bg-pink-400/8 hover:bg-default/40 rounded-full border-pink-400/20 dark:border-pink-400/10 dark:bg-white/10 dark:hover:bg-pink-400/20"
|
||||
href="/docs/changelog"
|
||||
className="text-muted chip bg-surface-secondary rounded-full text-xs"
|
||||
href="/docs/changelog/v3-0-0-beta-1"
|
||||
>
|
||||
<Iconify className="text-pink-400/90" icon="sparkles" />
|
||||
{/* <span className="bg-gradient-to-r from-[#CA8501] to-[#BD3232] bg-clip-text text-transparent"> */}
|
||||
<span className="text-pink-400/90">v{currentVersion}: RSC Support</span>
|
||||
<Iconify icon="gravity-ui:sparkles" />
|
||||
<span>v{currentVersion}: Major Redesign</span>
|
||||
</LinkRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export const dynamic = "force-static";
|
||||
export const revalidate = false;
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="flex h-[calc(100vh-4rem)] flex-col">
|
||||
@@ -27,29 +33,50 @@ export default function HomePage() {
|
||||
<section className="z-10 flex flex-col items-center pt-20 text-center">
|
||||
<div className="mx-auto flex max-w-2xl flex-col items-center justify-center gap-y-4">
|
||||
<VersionChip />
|
||||
<h1 className="text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
The design system you don't have to build
|
||||
<h1 className="text-foreground mt-2 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
|
||||
Beautiful by default. Customizable by design.
|
||||
</h1>
|
||||
<p className="text-muted text-balance text-lg">
|
||||
HeroUI is a set of beautiful, customizable, always up-to-date components that help you
|
||||
skip the copy-paste and focus on building.
|
||||
<p className="text-muted text-balance md:text-lg">
|
||||
HeroUI is the modern UI library built to help you move fast, stay consistent, and
|
||||
deliver delightful user experiences.
|
||||
</p>
|
||||
<div className="mt-2 flex gap-3">
|
||||
<LinkRoot
|
||||
className={buttonVariants({class: "shadow-lg", variant: "primary"})}
|
||||
href="/docs"
|
||||
>
|
||||
Explore Docs
|
||||
</LinkRoot>
|
||||
<LinkRoot
|
||||
className={buttonVariants({class: "bg-default/50", variant: "tertiary"})}
|
||||
className={buttonVariants({variant: "primary"})}
|
||||
href="/docs/components/accordion"
|
||||
>
|
||||
View Components
|
||||
View components
|
||||
</LinkRoot>
|
||||
<GitHubLink>Stars</GitHubLink>
|
||||
</div>
|
||||
</div>
|
||||
{/* <HomeThemeCustomizer /> */}
|
||||
{/* Mobile/Tablet: Show images */}
|
||||
<section className="-ml-4 mt-16 w-[100vw] overflow-hidden lg:hidden lg:w-[150vw]">
|
||||
<Image
|
||||
alt="HeroUI components preview"
|
||||
className="block dark:hidden"
|
||||
fetchPriority="high"
|
||||
height={1592}
|
||||
loading="eager"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 60vw"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/home-components-light.jpeg"
|
||||
width={2528}
|
||||
/>
|
||||
<Image
|
||||
alt="HeroUI components preview"
|
||||
className="hidden dark:block"
|
||||
fetchPriority="high"
|
||||
height={1592}
|
||||
loading="eager"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 60vw"
|
||||
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/docs/home-components-dark.jpeg"
|
||||
width={2528}
|
||||
/>
|
||||
</section>
|
||||
{/* Desktop: Show demos */}
|
||||
<div className="hidden lg:block">
|
||||
<DemoComponents />
|
||||
</div>
|
||||
</section>
|
||||
<footer className="text-muted mt-auto flex w-full flex-row flex-wrap items-center justify-center gap-2 py-3">
|
||||
<p className="text-sm">
|
||||
|
||||
@@ -1,5 +1,99 @@
|
||||
import type {NextRequest} from "next/server";
|
||||
|
||||
import {createFromSource} from "fumadocs-core/search/server";
|
||||
|
||||
import {source} from "@/lib/source";
|
||||
|
||||
export const {GET} = createFromSource(source);
|
||||
// Create a filtered source that excludes changelog pages
|
||||
const filteredSource = {
|
||||
...source,
|
||||
getPages() {
|
||||
return source.getPages().filter((page) => {
|
||||
// Filter out changelog pages by checking slugs, URL, and path
|
||||
const firstSlug = page.slugs[0];
|
||||
const url = page.url;
|
||||
const path = page.path;
|
||||
|
||||
return (
|
||||
firstSlug !== "changelog" && !url.includes("/changelog") && !path.includes("changelog")
|
||||
);
|
||||
});
|
||||
},
|
||||
} as typeof source;
|
||||
|
||||
const {GET: originalGET} = createFromSource(filteredSource);
|
||||
|
||||
// Wrap the GET handler to filter out changelog results from the response
|
||||
export async function GET(request: NextRequest) {
|
||||
const response = await originalGET(request);
|
||||
|
||||
// Clone the response to read it without consuming the original
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
|
||||
if (!contentType.includes("application/json")) {
|
||||
// If it's not JSON, return as-is (might be a stream or other format)
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await response.json();
|
||||
|
||||
// Helper function to check if an item is a changelog entry
|
||||
const isChangelog = (item: any): boolean => {
|
||||
const url = item?.url || item?.href || "";
|
||||
const id = item?.id || "";
|
||||
const title = item?.title || "";
|
||||
const path = item?.path || "";
|
||||
|
||||
return (
|
||||
url.includes("/changelog") ||
|
||||
id.includes("/changelog") ||
|
||||
path.includes("changelog") ||
|
||||
title.toLowerCase().includes("changelog")
|
||||
);
|
||||
};
|
||||
|
||||
// Filter out changelog results from the search response
|
||||
if (Array.isArray(data)) {
|
||||
const filtered = data.filter((item: any) => !isChangelog(item));
|
||||
|
||||
return Response.json(filtered, {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
}
|
||||
|
||||
// If the response structure is different, try to filter nested results
|
||||
if (data && typeof data === "object") {
|
||||
const filtered: any = {};
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (Array.isArray(value)) {
|
||||
filtered[key] = (value as any[]).filter((item: any) => !isChangelog(item));
|
||||
} else if (value && typeof value === "object" && "results" in value) {
|
||||
// Handle Orama-style search results
|
||||
filtered[key] = {
|
||||
...value,
|
||||
results: Array.isArray((value as any).results)
|
||||
? (value as any).results.filter((item: any) => !isChangelog(item))
|
||||
: (value as any).results,
|
||||
};
|
||||
} else {
|
||||
filtered[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(filtered, {
|
||||
headers: response.headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// If parsing fails, return the original response
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function Layout({children}: {children: ReactNode}) {
|
||||
defaultOpenLevel: 0,
|
||||
}}
|
||||
themeSwitch={{
|
||||
mode: "light-dark",
|
||||
mode: "light-dark-system",
|
||||
}}
|
||||
{...baseOptions}
|
||||
nav={{
|
||||
@@ -30,7 +30,6 @@ export default function Layout({children}: {children: ReactNode}) {
|
||||
title: <HeroUILogo />,
|
||||
}}
|
||||
>
|
||||
<div aria-hidden="true" className="gradient-background docs-gradient-background" />
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
+183
-161
@@ -20,7 +20,7 @@
|
||||
|
||||
/* Fumadocs UI Variables */
|
||||
--fd-layout-width: 1400px;
|
||||
--color-fd-background: #f9f9f9;
|
||||
--color-fd-background: var(--background);
|
||||
--color-fd-primary: oklch(0.14 0 0);
|
||||
--color-fd-secondary: #f9f9f9;
|
||||
--color-fd-card: #f9f9f9;
|
||||
@@ -39,6 +39,10 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
.light {
|
||||
}
|
||||
|
||||
/* Dark Theme Variables */
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
@@ -48,9 +52,8 @@
|
||||
|
||||
/* Fumadocs UI Dark Theme */
|
||||
--color-fd-primary: oklch(0.99 0 0);
|
||||
--color-fd-background: #1a1a1a;
|
||||
--color-fd-card: rgba(40, 40, 40, 0.4);
|
||||
--color-fd-secondary: #1a1a1a;
|
||||
--color-fd-secondary: #18181b;
|
||||
--color-fd-accent: #2c2c2c;
|
||||
--color-fd-popover: #383435;
|
||||
--color-fd-border: rgba(255, 255, 255, 0.1);
|
||||
@@ -66,10 +69,18 @@
|
||||
@apply rounded-sm;
|
||||
} */
|
||||
|
||||
/* Fumadocs UI Components Styles */
|
||||
/* ============================================================================
|
||||
FUMADOCS UI - NAVIGATION COMPONENTS
|
||||
============================================================================ */
|
||||
|
||||
[data-theme-toggle] {
|
||||
@apply bg-default/80 border-none;
|
||||
}
|
||||
|
||||
/* Home Layout Navigation */
|
||||
#nd-home-layout {
|
||||
#nd-nav {
|
||||
@apply bg-background blur-none backdrop-blur-none;
|
||||
[data-theme-toggle] {
|
||||
margin-left: 4px;
|
||||
}
|
||||
@@ -80,16 +91,7 @@
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
#nd-nav,
|
||||
#nd-subnav {
|
||||
[data-theme-toggle] {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Main Navigation */
|
||||
#nd-nav {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
@@ -111,10 +113,10 @@
|
||||
}
|
||||
|
||||
[data-search-full] {
|
||||
@apply bg-default border-border shadow-xs/5 rounded-xl;
|
||||
@apply bg-default/80 rounded-field;
|
||||
|
||||
kbd {
|
||||
@apply bg-default-hover border-border;
|
||||
@apply bg-surface text-muted px-1 font-sans text-xs font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,15 +125,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
#nd-nav {
|
||||
/* Sub Navigation */
|
||||
#nd-subnav {
|
||||
@apply bg-background h-14;
|
||||
|
||||
backdrop-filter: none;
|
||||
|
||||
> div {
|
||||
@apply border-none px-6;
|
||||
}
|
||||
|
||||
[data-search-full] {
|
||||
background: rgba(10, 10, 10, 0.1);
|
||||
@apply bg-field dark:bg-field/80 shadow-field rounded-field sm:max-w-[300px] md:max-w-[400px] lg:mr-12 dark:backdrop-blur-md;
|
||||
|
||||
kbd {
|
||||
@apply bg-default dark:bg-surface border-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
#nd-sidebar {
|
||||
@apply pt-0;
|
||||
|
||||
@@ -157,6 +170,8 @@
|
||||
}
|
||||
|
||||
#nd-sidebar a {
|
||||
@apply rounded-field;
|
||||
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
@@ -164,19 +179,18 @@
|
||||
}
|
||||
|
||||
&[data-active="true"] {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
@apply bg-surface shadow-surface;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
#nd-sidebar a {
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
/* Nested items inside open groups */
|
||||
#nd-sidebar [data-state="open"] > [data-state="open"] {
|
||||
padding: 4px 16px;
|
||||
|
||||
&[data-active="true"] {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
a {
|
||||
padding-left: 14px;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,88 +212,13 @@
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
#nd-sidebar a[rel="noreferrer noopener"]::after {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="10" height="10"><path d="M6 18L18 6m0 0H9m9 0v9" fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>');
|
||||
}
|
||||
}
|
||||
|
||||
#nd-sidebar a[rel="noreferrer noopener"]:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#nd-subnav {
|
||||
@apply h-14;
|
||||
|
||||
backdrop-filter: none;
|
||||
|
||||
> div {
|
||||
@apply border-none px-6;
|
||||
}
|
||||
|
||||
[data-search-full] {
|
||||
@apply bg-default border-border shadow-xs/5 rounded-xl sm:max-w-[300px] md:max-w-[400px] lg:mr-12;
|
||||
|
||||
kbd {
|
||||
@apply bg-default-hover border-border;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(16px) saturate(120%);
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
#nd-subnav {
|
||||
[data-search-full] {
|
||||
background: rgba(10, 10, 10, 0.4);
|
||||
backdrop-filter: blur(5px) saturate(120%);
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
background: rgba(10, 10, 10, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-background {
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background-image: linear-gradient(
|
||||
350deg,
|
||||
hsl(0 0% 98%) 0%,
|
||||
hsl(11 15% 95%) 24%,
|
||||
hsl(11 21% 92%) 61%,
|
||||
hsl(353 21% 92%) 81%,
|
||||
hsl(318 16% 93%) 90%,
|
||||
hsl(228 26% 96%) 100%
|
||||
);
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
.gradient-background {
|
||||
background-image: linear-gradient(
|
||||
340deg,
|
||||
hsl(0, 33%, 0%) 0%,
|
||||
hsl(0, 33%, 8%) 40%,
|
||||
hsl(284, 22%, 10%) 70%,
|
||||
hsl(233deg 24% 8%) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
/* ============================================================================
|
||||
FUMADOCS UI - LAYOUT COMPONENTS
|
||||
============================================================================ */
|
||||
|
||||
#nd-docs-layout {
|
||||
position: relative;
|
||||
@@ -308,13 +247,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.component-preview-tabs {
|
||||
[role="tab"] {
|
||||
border-bottom: none;
|
||||
.component-preview-container {
|
||||
/* Preview and code are now displayed vertically */
|
||||
|
||||
.code-section {
|
||||
.code-block-wrapper {
|
||||
@apply rounded-none rounded-b-xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
FUMADOCS UI - TABLE OF CONTENTS
|
||||
============================================================================ */
|
||||
|
||||
#nd-toc {
|
||||
padding-top: 10px;
|
||||
right: 0;
|
||||
@@ -333,29 +280,26 @@
|
||||
|
||||
#nd-tocnav {
|
||||
border-radius: 0rem;
|
||||
top: calc(var(--fd-banner-height) + var(--fd-nav-height) - 6px) !important;
|
||||
top: calc(var(--fd-banner-height) + var(--fd-nav-height) - 12px) !important;
|
||||
margin-right: 0rem;
|
||||
margin-left: 0rem;
|
||||
margin-top: 0px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(16px) saturate(120%) !important;
|
||||
background: var(--background);
|
||||
backdrop-filter: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
border-top: 1px solid var(--border) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
border-bottom: 1px solid var(--divider) !important;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
margin-right: 1rem;
|
||||
border-bottom: 0px solid var(--divider) !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
#nd-tocnav {
|
||||
background: rgba(10, 10, 10, 0.4);
|
||||
border: 1px solid rgba(60, 60, 60, 0.5);
|
||||
}
|
||||
}
|
||||
/* ============================================================================
|
||||
PROSE & CONTENT STYLES
|
||||
============================================================================ */
|
||||
|
||||
.prose {
|
||||
.shiki {
|
||||
@@ -394,30 +338,51 @@
|
||||
max-height: fit-content;
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
.prose {
|
||||
.shiki {
|
||||
background: transparent;
|
||||
[role="tabpanel"] .mdx-code-block {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
figure {
|
||||
background: red;
|
||||
code {
|
||||
@apply border-divider bg-transparent px-[3px];
|
||||
}
|
||||
|
||||
/* Table styles for .prose-no-margin */
|
||||
.prose-no-margin {
|
||||
table {
|
||||
@apply border-none bg-transparent;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
@apply bg-default dark:bg-default/50 border-none;
|
||||
|
||||
&:first-child {
|
||||
@apply rounded-bl-xl rounded-tl-xl;
|
||||
}
|
||||
|
||||
/* Scroll container */
|
||||
.fd-scroll-container {
|
||||
background: rgba(40, 40, 40, 0.4);
|
||||
&:last-child {
|
||||
@apply rounded-br-xl rounded-tr-xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-code-block {
|
||||
.fd-scroll-container {
|
||||
background: transparent !important;
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
@apply border-none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
CODE BLOCK STYLES
|
||||
============================================================================ */
|
||||
|
||||
.docs-code-block {
|
||||
@apply m-0 rounded-none border-none pl-2 pr-2 pt-4;
|
||||
|
||||
@@ -430,29 +395,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.shiki:not(.not-fumadocs-codeblock *) code .highlighted {
|
||||
border-radius: 4px;
|
||||
background-color: #d6d8e3 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
.shiki:not(.not-fumadocs-codeblock *) code .highlighted {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
/* Apply !important only for isolated code blocks (not code-block-wrapper) */
|
||||
.is-isolated .docs-code-block {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
@apply rounded-xl;
|
||||
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
.code-block-wrapper {
|
||||
background: rgba(40, 40, 40, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.docs-code-block-line-numbers {
|
||||
@@ -472,7 +421,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrolling Banner Animations */
|
||||
.shiki:not(.not-fumadocs-codeblock *) code .highlighted {
|
||||
border-radius: 4px;
|
||||
background-color: #d6d8e3 !important;
|
||||
}
|
||||
|
||||
.showcase-source-wrapper {
|
||||
.code-block-wrapper {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.fd-scroll-container {
|
||||
max-height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
ANIMATIONS & KEYFRAMES
|
||||
============================================================================ */
|
||||
|
||||
@keyframes scrolling-banner {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
@@ -517,7 +485,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation utility classes */
|
||||
/* ============================================================================
|
||||
UTILITY CLASSES
|
||||
============================================================================ */
|
||||
|
||||
.animate-scrolling-banner {
|
||||
animation: scrolling-banner var(--duration) linear infinite;
|
||||
}
|
||||
@@ -534,18 +505,69 @@
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 1) 70%,
|
||||
rgba(0, 0, 0, 1) 20%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.showcase-source-wrapper {
|
||||
.code-block-wrapper {
|
||||
background: transparent !important;
|
||||
/* ============================================================================
|
||||
DARK THEME OVERRIDES
|
||||
============================================================================ */
|
||||
|
||||
[data-theme="dark"],
|
||||
.dark {
|
||||
/* Navigation Components */
|
||||
|
||||
#nd-sidebar a {
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.fd-scroll-container {
|
||||
max-height: 100%;
|
||||
&[data-active="true"] {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
#nd-sidebar a[rel="noreferrer noopener"]::after {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="10" height="10"><path d="M6 18L18 6m0 0H9m9 0v9" fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>');
|
||||
}
|
||||
|
||||
/* Table of Contents */
|
||||
#nd-tocnav {
|
||||
background: rgba(10, 10, 10, 0.4);
|
||||
border: 1px solid rgba(60, 60, 60, 0.5);
|
||||
}
|
||||
|
||||
/* Prose & Code Blocks */
|
||||
.prose {
|
||||
.shiki {
|
||||
background: transparent;
|
||||
|
||||
figure {
|
||||
background: red;
|
||||
}
|
||||
|
||||
/* Scroll container */
|
||||
.fd-scroll-container {
|
||||
background: rgba(40, 40, 40, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-code-block {
|
||||
background: rgba(40, 40, 40, 0.4);
|
||||
|
||||
.fd-scroll-container {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply !important only for isolated code blocks (not code-block-wrapper) */
|
||||
.is-isolated .docs-code-block {
|
||||
background: rgba(40, 40, 40, 0.4) !important;
|
||||
}
|
||||
|
||||
.shiki:not(.not-fumadocs-codeblock *) code .highlighted {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {HeroUILogo} from "@/components/heroui-logo";
|
||||
* Docs Layout: app/docs/layout.tsx
|
||||
*/
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
githubUrl: "https://github.com/heroui-inc/heroui",
|
||||
nav: {
|
||||
title: <HeroUILogo />,
|
||||
transparentMode: "always",
|
||||
|
||||
@@ -81,6 +81,10 @@ export const metadata: Metadata = {
|
||||
type: "website",
|
||||
url: "/",
|
||||
},
|
||||
robots: {
|
||||
follow: true,
|
||||
index: true,
|
||||
},
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s | ${siteConfig.name}`,
|
||||
@@ -96,11 +100,10 @@ export const metadata: Metadata = {
|
||||
|
||||
export const viewport: Viewport = {
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
maximumScale: 5,
|
||||
themeColor: [
|
||||
{color: "#f4f4f5", media: "(prefers-color-scheme: light)"},
|
||||
{color: "#111111", media: "(prefers-color-scheme: dark)"},
|
||||
],
|
||||
userScalable: false,
|
||||
width: "device-width",
|
||||
};
|
||||
|
||||
@@ -31,11 +31,8 @@ function generate({...props}: GenerateProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(350deg, #000 0%, rgba(31, 20, 21, .7) 15%, rgba(41, 26, 27, .6) 30%, rgba(37, 24, 28, .65) 40%, rgba(33, 23, 29, .65) 50%, rgba(30, 21, 30, .65) 60%, #1c151e 70%, #18131b 78%, #15121c 85%, #111019 92%, #101119 100%),
|
||||
linear-gradient(350deg, #000 0%, #291a1b 40%, #1c151e 70%, #15121c 85%, #101119 100%)
|
||||
`,
|
||||
color: "#FAFAFA",
|
||||
background: "#F5F5F5",
|
||||
color: "#18181B",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
@@ -56,7 +53,7 @@ function generate({...props}: GenerateProps): ReactElement {
|
||||
{/* Description */}
|
||||
<p
|
||||
style={{
|
||||
color: "#A1A1A1",
|
||||
color: "#71717A",
|
||||
fontSize: "52px",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ShowcaseCodePanel({
|
||||
initial={{opacity: 0, x: "100%"}}
|
||||
transition={{bounce: 0, duration: 0.4, type: "spring"}}
|
||||
className={cn(
|
||||
"bg-surface-2/94 absolute right-6 z-[9999] h-full max-h-[88%] w-[45%] overflow-hidden rounded-xl shadow-xl backdrop-blur-md",
|
||||
"bg-surface/94 absolute right-6 z-[9999] h-full max-h-[88%] w-[45%] overflow-hidden rounded-xl shadow-xl backdrop-blur-md",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function LLMCopyButton({
|
||||
<button
|
||||
disabled={isLoading}
|
||||
className={docsButtonVariants({
|
||||
className: "[&_svg]:text-muted [&_svg]:size-3.5",
|
||||
className: "[&_svg]:text-muted text-sm [&_svg]:size-3.5",
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -12,10 +12,12 @@ export function CodeBlock({
|
||||
children,
|
||||
className,
|
||||
collapsible,
|
||||
isIsolated = false,
|
||||
showLineNumbers,
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
isIsolated?: boolean;
|
||||
lang?: string;
|
||||
collapsible?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
@@ -30,6 +32,7 @@ export function CodeBlock({
|
||||
title={title}
|
||||
className={cn(
|
||||
"code-block-wrapper docs-code-block",
|
||||
isIsolated && "is-isolated",
|
||||
showLineNumbers && "docs-code-block-line-numbers",
|
||||
className,
|
||||
)}
|
||||
@@ -45,7 +48,8 @@ export function CodeBlock({
|
||||
<div
|
||||
className={cn(
|
||||
"code-block-wrapper",
|
||||
isCollapsed && "mask-to-bottom relative max-h-[300px] overflow-hidden",
|
||||
isIsolated && "is-isolated",
|
||||
isCollapsed && "mask-to-bottom relative max-h-[150px] overflow-hidden",
|
||||
!isCollapsed && "pb-10",
|
||||
)}
|
||||
>
|
||||
@@ -62,10 +66,10 @@ export function CodeBlock({
|
||||
</Base.CodeBlock>
|
||||
</div>
|
||||
<Button
|
||||
className={cn("absolute bottom-0 right-1/2 translate-x-1/2", !isCollapsed && "bottom-4")}
|
||||
className="bg-surface absolute bottom-2 right-1/2 translate-x-1/2 text-xs shadow-sm shadow-black/5"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
variant="tertiary"
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
{isCollapsed ? "Expand code" : "Collapse code"}
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function CodeBlock({
|
||||
className,
|
||||
code,
|
||||
collapsible,
|
||||
isIsolated = false,
|
||||
lang,
|
||||
showLineNumbers,
|
||||
title,
|
||||
@@ -17,6 +18,8 @@ export async function CodeBlock({
|
||||
}: {
|
||||
code: string;
|
||||
lang: string;
|
||||
// Means the code block is rendered directly in the MDX file using <CollapsibleCode>
|
||||
isIsolated?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
title: string | undefined;
|
||||
collapsible?: boolean;
|
||||
@@ -57,6 +60,7 @@ export async function CodeBlock({
|
||||
<CodeBlockClient
|
||||
className={className}
|
||||
collapsible={collapsible}
|
||||
isIsolated={isIsolated}
|
||||
lang={lang}
|
||||
showLineNumbers={showLineNumbers}
|
||||
title={title}
|
||||
|
||||
@@ -17,6 +17,7 @@ export function CollapsibleCode({
|
||||
}) {
|
||||
return (
|
||||
<CodeBlock
|
||||
isIsolated
|
||||
className={className}
|
||||
code={code}
|
||||
collapsible={collapsible}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function ColorSwatch({className, foreground, name, value, variable}: Colo
|
||||
return (
|
||||
<div className={cn("flex flex-col items-center gap-2", className)}>
|
||||
<button
|
||||
className="border-border focus:ring-accent group relative size-20 rounded-lg border shadow-sm transition-all hover:scale-105 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
className="focus:ring-accent shadow-surface group relative size-10 rounded-full transition-all hover:scale-105 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
type="button"
|
||||
style={{
|
||||
backgroundColor: `var(${variable})`,
|
||||
@@ -111,7 +111,7 @@ export function ColorSwatch({className, foreground, name, value, variable}: Colo
|
||||
</div>
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium">{name}</div>
|
||||
<div className="text-xs font-medium">{variable}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,8 +17,7 @@ export function ComponentItem({className, component, ...props}: ComponentItemPro
|
||||
<a className="block" href={href} rel="noopener noreferrer" target="_blank">
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/60 group relative flex h-full flex-col gap-3 rounded-xl border bg-transparent p-5 py-4 transition-all duration-200",
|
||||
"hover:border-border hover:bg-foreground/2",
|
||||
"bg-surface group relative flex h-full flex-col gap-3 rounded-xl p-5 py-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const ComponentLinks = ({links}: ComponentLinksProps) => {
|
||||
) : null}
|
||||
{componentLinks.storybook ? (
|
||||
<ButtonLink
|
||||
href={`${componentLinks.storybook}--default`}
|
||||
href={`${componentLinks.storybook}--docs`}
|
||||
startContent={<StorybookIcon className="text-lg text-[#ff4785]" />}
|
||||
>
|
||||
Storybook
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {useIntersectionObserver} from "usehooks-ts";
|
||||
|
||||
import {cn} from "@/utils/cn";
|
||||
|
||||
interface ComponentPreviewContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
align?: "center" | "start" | "end";
|
||||
isBgSolid?: boolean;
|
||||
description?: string;
|
||||
hideCode?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function ComponentPreviewContainer({
|
||||
align = "center",
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
hideCode = false,
|
||||
isBgSolid = false,
|
||||
name,
|
||||
...props
|
||||
}: React.PropsWithChildren<ComponentPreviewContainerProps>) {
|
||||
const [Component, Code] = React.Children.toArray(children) as React.ReactElement[];
|
||||
const {isIntersecting: previewInView, ref: previewRef} = useIntersectionObserver({
|
||||
initialIsIntersecting: true,
|
||||
rootMargin: "400px 0px 100px 0px",
|
||||
threshold: 0,
|
||||
});
|
||||
const {isIntersecting: codeInView, ref: codeRef} = useIntersectionObserver({
|
||||
initialIsIntersecting: true,
|
||||
rootMargin: "300px 0px 0px 0px",
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
const alignmentClasses = {
|
||||
center: "items-center justify-center",
|
||||
end: "items-end justify-end",
|
||||
start: "items-start justify-start",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("component-preview-container group relative my-4 w-full", className)}
|
||||
data-name={name}
|
||||
{...props}
|
||||
>
|
||||
{!!description && <p className="text-muted-foreground mb-2 text-sm">{description}</p>}
|
||||
|
||||
{/* Preview Section */}
|
||||
<div
|
||||
ref={previewRef}
|
||||
data-name={name}
|
||||
className={cn(
|
||||
"preview not-prose border-divider relative min-h-[350px] w-full overflow-hidden rounded-t-xl border-l border-r border-t p-4 sm:p-10",
|
||||
isBgSolid && "bg-background",
|
||||
alignmentClasses[align],
|
||||
"flex",
|
||||
)}
|
||||
style={{
|
||||
contain: "layout style",
|
||||
contentVisibility: "auto",
|
||||
isolation: "isolate",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex w-full items-center justify-center"
|
||||
style={{
|
||||
pointerEvents: previewInView ? "auto" : "none",
|
||||
visibility: previewInView ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{Component}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Section */}
|
||||
{!hideCode && !!Code && (
|
||||
<div
|
||||
ref={codeRef}
|
||||
className="code-section border-divider relative rounded-b-xl border bg-transparent"
|
||||
style={{
|
||||
contentVisibility: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="code-block-wrapper min-h-[124px]"
|
||||
style={{
|
||||
pointerEvents: codeInView ? "auto" : "none",
|
||||
visibility: codeInView ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{Code}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import {Tab, Tabs} from "fumadocs-ui/components/tabs";
|
||||
import React from "react";
|
||||
|
||||
import {cn} from "@/utils/cn";
|
||||
|
||||
interface ComponentPreviewTabsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
align?: "center" | "start" | "end";
|
||||
isBgSolid?: boolean;
|
||||
description?: string;
|
||||
hideCode?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function ComponentPreviewTabs({
|
||||
align = "center",
|
||||
children,
|
||||
className,
|
||||
description,
|
||||
hideCode = false,
|
||||
isBgSolid = false,
|
||||
name,
|
||||
...props
|
||||
}: React.PropsWithChildren<ComponentPreviewTabsProps>) {
|
||||
const [Component, Code] = React.Children.toArray(children) as React.ReactElement[];
|
||||
|
||||
const alignmentClasses = {
|
||||
center: "items-center justify-center",
|
||||
end: "items-end justify-end",
|
||||
start: "items-start justify-start",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("component-preview-tabs group relative my-4 w-full", className)}
|
||||
data-name={name}
|
||||
{...props}
|
||||
>
|
||||
{!!description && <p className="text-muted-foreground mb-2 text-sm">{description}</p>}
|
||||
<Tabs
|
||||
className="relative w-full border-none bg-transparent p-0"
|
||||
items={hideCode ? ["Preview"] : ["Preview", "Code"]}
|
||||
>
|
||||
<Tab className="border-border overflow-hidden border bg-transparent p-0" value="Preview">
|
||||
<div
|
||||
data-name={name}
|
||||
className={cn(
|
||||
"preview not-prose relative min-h-[350px] w-full overflow-hidden rounded-md p-4 before:absolute before:inset-0 before:z-[-1] before:bg-white/40 sm:p-10 dark:before:bg-black/25",
|
||||
isBgSolid && "bg-background",
|
||||
alignmentClasses[align],
|
||||
"flex",
|
||||
)}
|
||||
style={{
|
||||
contain: "layout style",
|
||||
isolation: "isolate",
|
||||
}}
|
||||
>
|
||||
{Component}
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
{!hideCode && !!Code && (
|
||||
<Tab className="bg-transparent p-0" value="Code">
|
||||
<div className="relative">
|
||||
<div className="code-block-wrapper max-h-[650px] min-h-[350px] overflow-auto">
|
||||
{Code}
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import {getDemo} from "@/demos";
|
||||
import {cn} from "@/utils/cn";
|
||||
|
||||
import {ComponentPreviewTabs} from "./component-preview-tabs";
|
||||
import {ComponentPreviewContainer} from "./component-preview-container";
|
||||
import {ComponentSource} from "./component-source";
|
||||
|
||||
interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@@ -40,7 +40,7 @@ export function ComponentPreview({
|
||||
const Component = demo.component;
|
||||
|
||||
return (
|
||||
<ComponentPreviewTabs
|
||||
<ComponentPreviewContainer
|
||||
align={align}
|
||||
className={className}
|
||||
description={description}
|
||||
@@ -51,6 +51,6 @@ export function ComponentPreview({
|
||||
>
|
||||
<Component />
|
||||
{!hideCode && !!demo.file && <ComponentSource language="tsx" name={name} title={name} />}
|
||||
</ComponentPreviewTabs>
|
||||
</ComponentPreviewContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {CodeBlock} from "./codeblock";
|
||||
export async function ComponentSource({
|
||||
className,
|
||||
// TODO: Disclosure is not ready yet, so we need to use collapsible for now
|
||||
collapsible = false,
|
||||
collapsible = true,
|
||||
language,
|
||||
name,
|
||||
showCodeTitle = false,
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface DocsImageProps {
|
||||
src: string;
|
||||
darkSrc?: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
export function DocsImage({
|
||||
alt,
|
||||
className = "h-[220px] md:h-[340px]",
|
||||
darkSrc,
|
||||
height = 1000,
|
||||
priority = true,
|
||||
src,
|
||||
width = 1000,
|
||||
}: DocsImageProps) {
|
||||
const wrapperClasses = `not-prose relative w-full overflow-hidden rounded-xl shadow-panel ${className}`;
|
||||
const wrapperClasses = `not-prose border border-divider relative w-full overflow-hidden rounded-xl ${className}`;
|
||||
|
||||
if (darkSrc) {
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<img
|
||||
<Image
|
||||
alt={alt}
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-0 transition-opacity duration-300 ease-in-out dark:opacity-100"
|
||||
src={darkSrc}
|
||||
/>
|
||||
<img
|
||||
alt={alt}
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-100 transition-opacity duration-300 ease-in-out dark:opacity-0"
|
||||
className="absolute inset-0 block h-full w-full object-cover dark:hidden"
|
||||
height={height}
|
||||
priority={priority}
|
||||
src={src}
|
||||
width={width}
|
||||
/>
|
||||
<Image
|
||||
alt={alt}
|
||||
className="absolute inset-0 hidden h-full w-full object-cover dark:block"
|
||||
height={height}
|
||||
priority={priority}
|
||||
src={darkSrc}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -34,7 +46,14 @@ export function DocsImage({
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<img alt={alt} className="absolute inset-0 h-full w-full object-cover" src={src} />
|
||||
<Image
|
||||
alt={alt}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
height={height}
|
||||
priority={priority}
|
||||
src={src}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {Skeleton, buttonVariants} from "@heroui/react";
|
||||
import * as React from "react";
|
||||
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {GitHubIcon} from "@/icons/github";
|
||||
import {cn} from "@/utils/cn";
|
||||
import {GITHUB_API_URL} from "@/utils/constants";
|
||||
|
||||
export function GitHubLink({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={siteConfig.links.github}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: cn("bg-surface-tertiary", className),
|
||||
variant: "tertiary",
|
||||
})}
|
||||
>
|
||||
<GitHubIcon />
|
||||
{children}
|
||||
<React.Suspense fallback={<Skeleton className="h-4 w-8" />}>
|
||||
<StarsCount />
|
||||
</React.Suspense>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export async function StarsCount() {
|
||||
const data = await fetch(`${GITHUB_API_URL}/repos/${siteConfig.githubRepo}`, {
|
||||
next: {revalidate: 86400}, // Cache for 1 day (86400 seconds)
|
||||
});
|
||||
const json = await data.json();
|
||||
|
||||
return (
|
||||
<span className="text-muted pt-px text-xs font-medium">
|
||||
{json.stargazers_count >= 1000
|
||||
? `${(json.stargazers_count / 1000).toFixed(1)}k`
|
||||
: json.stargazers_count.toLocaleString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type {IconProps} from "@iconify/react";
|
||||
|
||||
import {Icon} from "@iconify/react";
|
||||
@@ -34,15 +32,17 @@ const icons = {
|
||||
...customIcons,
|
||||
};
|
||||
|
||||
// TODO: Hydration error
|
||||
const Iconify = forwardRef<SVGSVGElement, IconifyProps>(({icon: iconProp, ...props}, ref) => {
|
||||
// Check if it's a gravity-ui icon (no prefix or explicitly in gravity icons)
|
||||
const isGravityIcon =
|
||||
typeof iconProp === "string" && (iconProp in icons || !iconProp.includes(":"));
|
||||
typeof iconProp === "string" && (iconProp in icons || iconProp.startsWith("gravity-ui:"));
|
||||
|
||||
// Only use offline icon after hydration to avoid mismatch
|
||||
if (isGravityIcon && typeof iconProp === "string") {
|
||||
// Use offline version with gravity-ui icons
|
||||
const gravityIconData = icons[iconProp as keyof typeof icons];
|
||||
// Remove "gravity-ui:" prefix if present
|
||||
const iconName = iconProp.replace(/^gravity-ui:/, "");
|
||||
const gravityIconData = icons[iconName as keyof typeof icons];
|
||||
|
||||
if (gravityIconData) {
|
||||
return <OfflineIcon {...props} ref={ref} icon={gravityIconData} />;
|
||||
@@ -50,6 +50,7 @@ const Iconify = forwardRef<SVGSVGElement, IconifyProps>(({icon: iconProp, ...pro
|
||||
}
|
||||
|
||||
// Use online version for other icon sets (like simple-icons:vite, lineicons:nextjs)
|
||||
// Also use during SSR to ensure consistent rendering
|
||||
return <Icon {...props} ref={ref} icon={iconProp} />;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {Button, Spinner} from "@heroui/react";
|
||||
import {Button, FieldError, Form, Input, Label, Spinner, TextField} from "@heroui/react";
|
||||
import {AnimatePresence, LazyMotion, domAnimation} from "motion/react";
|
||||
import * as m from "motion/react-m";
|
||||
import React from "react";
|
||||
@@ -8,7 +8,7 @@ import React from "react";
|
||||
export function NewsletterForm() {
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [status, setStatus] = React.useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [errorMessage, setErrorMessage] = React.useState("Something went wrong. Please try again.");
|
||||
const [apiErrorMessage, setApiErrorMessage] = React.useState<string | null>(null);
|
||||
|
||||
const circleCheckIcon = (
|
||||
<svg fill="none" height="16" viewBox="0 0 17 16" width="17" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -40,20 +40,21 @@ export function NewsletterForm() {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
setStatus("error");
|
||||
setErrorMessage("Please enter a valid email.");
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const emailValue = formData.get("email") as string;
|
||||
|
||||
if (!emailValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("loading");
|
||||
setApiErrorMessage(null);
|
||||
|
||||
try {
|
||||
// Loops - newsletter
|
||||
const newsletterResponse = await fetch("/api/newsletter", {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
email: emailValue,
|
||||
source: "Subscribe from HeroUI Docs",
|
||||
}),
|
||||
headers: {
|
||||
@@ -71,7 +72,7 @@ export function NewsletterForm() {
|
||||
// Featurebase - changelog
|
||||
const changelogResponse = await fetch("/api/changelog", {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
email: emailValue,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -90,7 +91,7 @@ export function NewsletterForm() {
|
||||
setTimeout(() => setStatus("idle"), 3000);
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
setApiErrorMessage(
|
||||
error instanceof Error ? error.message : "Something went wrong. Please try again.",
|
||||
);
|
||||
setTimeout(() => setStatus("idle"), 3000);
|
||||
@@ -100,36 +101,38 @@ export function NewsletterForm() {
|
||||
return (
|
||||
<div className="relative w-full py-3">
|
||||
<div className="relative h-[172px] w-full">
|
||||
<form noValidate className="flex flex-col gap-2" onSubmit={handleSubmit}>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<label
|
||||
className={`pb-1 text-sm font-medium tracking-[-0.07px] ${status === "error" ? "text-[#e44c3d] dark:text-[#eb5545]" : "text-muted"}`}
|
||||
htmlFor="newsletter-email"
|
||||
>
|
||||
Hero Newsletter
|
||||
</label>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
required
|
||||
disabled={status === "loading"}
|
||||
id="newsletter-email"
|
||||
placeholder="name@email.com"
|
||||
<Form className="flex flex-col gap-2" onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
isRequired
|
||||
className="flex w-full flex-col gap-1"
|
||||
isDisabled={status === "loading"}
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
className={`text-foreground placeholder:text-foreground-muted/80 min-h-8 w-full rounded-xl border px-3 py-2 text-[14px] tracking-[-0.28px] shadow-[0px_1px_2px_0px_rgba(0,0,0,0.05),0px_1px_1px_0px_inset_rgba(255,255,255,0.1)] transition-all duration-150 ease-out focus:outline-none ${
|
||||
status === "error"
|
||||
? "border-danger bg-white/0 dark:bg-black/0"
|
||||
: "border-black/[0.04] bg-black/[0.07] hover:bg-black/[0.1] dark:bg-white/[0.14] dark:hover:bg-white/[0.18]"
|
||||
}`}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
{status === "error" && (
|
||||
<p className="text-danger mt-1 px-1 text-xs">{errorMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
validate={(value) => {
|
||||
if (!value) {
|
||||
return "Email is required";
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||
return "Please enter a valid email.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
onChange={setEmail}
|
||||
>
|
||||
<Label className="pb-1 text-sm font-medium tracking-[-0.07px] after:hidden">
|
||||
Hero Newsletter
|
||||
</Label>
|
||||
<Input placeholder="name@email.com" value={email} />
|
||||
<FieldError className="mt-1 px-1 text-xs" />
|
||||
{apiErrorMessage && status === "error" ? (
|
||||
<p className="text-danger mt-1 px-1 text-xs">{apiErrorMessage}</p>
|
||||
) : null}
|
||||
</TextField>
|
||||
<Button
|
||||
className="overflow-clip border-black/[0.1] bg-[#FAFAFA]/[0.7] text-sm transition-all hover:bg-[#F5F5F5] dark:border-white/[0.1] dark:bg-[#171717]/[0.7] dark:hover:bg-[#262626]"
|
||||
className="w-full"
|
||||
isPending={status === "loading" || status === "success"}
|
||||
type="submit"
|
||||
variant="tertiary"
|
||||
@@ -152,7 +155,7 @@ export function NewsletterForm() {
|
||||
</AnimatePresence>
|
||||
</LazyMotion>
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import type {FC} from "react";
|
||||
|
||||
import {Button, Spinner, Tooltip, cn} from "@heroui/react";
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import {useIntersectionObserver} from "usehooks-ts";
|
||||
|
||||
import {useIsMobileDevice} from "@/hooks/use-is-mobile-device";
|
||||
|
||||
import {Iconify} from "./iconify";
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src: string;
|
||||
playMode?: "auto" | "manual";
|
||||
autoPlay?: boolean;
|
||||
poster?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
controls?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VideoPlayer: FC<VideoPlayerProps> = ({
|
||||
autoPlay = true,
|
||||
className,
|
||||
controls = false,
|
||||
height,
|
||||
playMode = "auto",
|
||||
poster,
|
||||
src,
|
||||
width,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const isMobile = useIsMobileDevice();
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const {isIntersecting: isVisible, ref: intersectionRef} = useIntersectionObserver({
|
||||
threshold: 0.5,
|
||||
});
|
||||
|
||||
// Merge refs: videoRef for video operations and intersectionRef for intersection observer
|
||||
const setVideoRef = useCallback(
|
||||
(element: HTMLVideoElement | null) => {
|
||||
videoRef.current = element;
|
||||
if (element) {
|
||||
if (typeof intersectionRef === "function") {
|
||||
intersectionRef(element);
|
||||
} else if (intersectionRef && "current" in intersectionRef) {
|
||||
(intersectionRef as React.MutableRefObject<HTMLVideoElement | null>).current = element;
|
||||
}
|
||||
}
|
||||
},
|
||||
[intersectionRef],
|
||||
);
|
||||
|
||||
// Determine effective play mode: force manual on mobile devices
|
||||
const effectivePlayMode = isMobile ? "manual" : playMode;
|
||||
|
||||
// play video when it is visible and playMode is auto (only on non-mobile devices)
|
||||
useEffect(() => {
|
||||
if (effectivePlayMode !== "auto" || !videoRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
videoRef.current.play();
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
videoRef.current.pause();
|
||||
setIsPlaying(false);
|
||||
}
|
||||
}, [isVisible, effectivePlayMode]);
|
||||
|
||||
const handleCanPlay = useCallback(() => {
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current;
|
||||
|
||||
if (videoEl) {
|
||||
if (videoEl.readyState > 3) {
|
||||
// HAVE_FUTURE_DATA: enough data to start playing
|
||||
handleCanPlay();
|
||||
} else {
|
||||
videoEl.addEventListener("canplaythrough", handleCanPlay);
|
||||
}
|
||||
|
||||
// Cleanup the event listener
|
||||
return () => {
|
||||
videoEl.removeEventListener("canplaythrough", handleCanPlay);
|
||||
};
|
||||
}
|
||||
}, [handleCanPlay]);
|
||||
|
||||
const onTogglePlay = useCallback(() => {
|
||||
if (videoRef.current) {
|
||||
if (!isPlaying) {
|
||||
videoRef.current.play();
|
||||
} else {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
|
||||
setIsPlaying((v) => !v);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="not-prose border-divider relative overflow-hidden rounded-xl border"
|
||||
data-playing={isPlaying}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
color="accent"
|
||||
size="lg"
|
||||
/>
|
||||
) : !isPlaying ? (
|
||||
<Tooltip delay={1000}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="absolute left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 bg-transparent before:absolute before:inset-0 before:z-[-1] before:block before:rounded-lg before:bg-black/10 before:backdrop-blur-md before:backdrop-saturate-150 before:content-['']"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onPress={onTogglePlay}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Iconify icon="gravity-ui:pause-fill" size={16} />
|
||||
) : (
|
||||
<Iconify icon="gravity-ui:play-fill" size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>{isPlaying ? "Pause" : "Play"}</Tooltip.Content>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
<video
|
||||
ref={setVideoRef}
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
autoPlay={!!autoPlay && effectivePlayMode === "auto"}
|
||||
className={cn("object-fit aspect-video w-full", className)}
|
||||
controls={controls}
|
||||
height={height}
|
||||
poster={poster}
|
||||
src={src}
|
||||
width={width}
|
||||
onCanPlay={handleCanPlay}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,8 @@ export const siteConfig = {
|
||||
figmaCommunityFile: "https://www.figma.com/community/file/1546526812159103429",
|
||||
githubRawUrl:
|
||||
"https://raw.githubusercontent.com/heroui-inc/heroui/refs/heads/v3/apps/docs/content/docs",
|
||||
githubRepo: "heroui-inc/heroui",
|
||||
githubUrl: "https://github.com/heroui-inc/heroui",
|
||||
links: {
|
||||
discord: "https://discord.gg/9b6yyZKmH4",
|
||||
github: "https://github.com/heroui-inc",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {Accordion} from "@heroui/react";
|
||||
import {Icon} from "@iconify/react";
|
||||
|
||||
@@ -43,7 +41,7 @@ const items = [
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Accordion.Root className="w-full max-w-md">
|
||||
<Accordion className="w-full max-w-md">
|
||||
{items.map((item, index) => (
|
||||
<Accordion.Item key={index}>
|
||||
<Accordion.Heading>
|
||||
@@ -62,6 +60,6 @@ export function Basic() {
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user