├── docs ├── app │ ├── favicon.ico │ ├── global.css │ ├── (home) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ └── search │ │ │ └── route.ts │ ├── docs │ │ ├── layout.tsx │ │ └── [[...slug]] │ │ │ └── page.tsx │ └── layout.tsx ├── postcss.config.mjs ├── next.config.mjs ├── lib │ ├── source.ts │ └── layout.shared.tsx ├── mdx-components.tsx ├── .gitignore ├── source.config.ts ├── tsconfig.json ├── package.json ├── README.md ├── content │ └── docs │ │ ├── components │ │ ├── alpha.mdx │ │ ├── hue.mdx │ │ ├── eye-dropper.mdx │ │ ├── saturation.mdx │ │ └── color-picker.mdx │ │ ├── index.mdx │ │ ├── hooks │ │ └── use-color-state.mdx │ │ └── utils.mdx └── components │ └── ColorPickerExample.tsx ├── src ├── styles.css ├── utils.ts ├── index.ts ├── utils │ ├── index.ts │ ├── internal.ts │ └── public.ts ├── components │ ├── Pointer.tsx │ ├── Hue.tsx │ ├── Saturation.tsx │ ├── Alpha.tsx │ ├── Interactive.tsx │ └── ColorPicker.tsx ├── hooks │ └── useColorState.ts └── types.ts ├── .prettierrc.json ├── .npmignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── eslint.config.mjs ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /docs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddoemonn/react-beautiful-color/HEAD/docs/app/favicon.ico -------------------------------------------------------------------------------- /docs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/app/global.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import 'fumadocs-ui/css/neutral.css'; 3 | @import 'fumadocs-ui/css/preset.css'; 4 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @source "./components/**/*.{ts,tsx}"; 4 | 5 | @utility rounded-inherit { 6 | border-radius: inherit; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Import from organized utilities for backward compatibility 2 | export { cn, parseColorString } from './utils/internal'; 3 | export * from './utils/public'; 4 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from 'fumadocs-mdx/next'; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | }; 9 | 10 | export default withMDX(config); 11 | -------------------------------------------------------------------------------- /docs/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HomeLayout } from 'fumadocs-ui/layouts/home'; 2 | import { baseOptions } from '@/lib/layout.shared'; 3 | 4 | export default function Layout({ children }: LayoutProps<'/'>) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from '@/lib/source'; 2 | import { createFromSource } from 'fumadocs-core/search/server'; 3 | 4 | export const { GET } = createFromSource(source, { 5 | // https://docs.orama.com/docs/orama-js/supported-languages 6 | language: 'english', 7 | }); 8 | -------------------------------------------------------------------------------- /docs/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs } from '@/.source'; 2 | import { loader } from 'fumadocs-core/source'; 3 | 4 | // See https://fumadocs.vercel.app/docs/headless/source-api for more info 5 | export const source = loader({ 6 | // it assigns a URL to your pages 7 | baseUrl: '/docs', 8 | source: docs.toFumadocsSource(), 9 | }); 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "singleAttributePerLine": true, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "arrowParens": "avoid", 9 | "endOfLine": "auto", 10 | "trailingComma": "es5", 11 | "plugins": ["prettier-plugin-tailwindcss"] 12 | } 13 | -------------------------------------------------------------------------------- /docs/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import defaultMdxComponents from 'fumadocs-ui/mdx'; 2 | import type { MDXComponents } from 'mdx/types'; 3 | 4 | // use this function to get MDX components, you will need it for rendering MDX 5 | export function getMDXComponents(components?: MDXComponents): MDXComponents { 6 | return { 7 | ...defaultMdxComponents, 8 | ...components, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .contentlayer 6 | .content-collections 7 | .source 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /docs/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from 'fumadocs-ui/layouts/docs'; 2 | import { baseOptions } from '@/lib/layout.shared'; 3 | import { source } from '@/lib/source'; 4 | 5 | export default function Layout({ children }: LayoutProps<'/docs'>) { 6 | return ( 7 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Import CSS styles 2 | import './styles.css'; 3 | 4 | export { ColorPicker } from './components/ColorPicker'; 5 | 6 | export { useColorState } from './hooks/useColorState'; 7 | 8 | // Export color utilities for public use 9 | export * from './utils'; 10 | 11 | export type { ColorFormat, ColorPickerProps, HexColor, HslaColor, HslColor, HsvaColor, HsvColor, RgbaColor, RgbColor } from './types'; 12 | 13 | export { Color } from './types'; 14 | 15 | export const version = '2.0.0'; 16 | -------------------------------------------------------------------------------- /docs/source.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | defineDocs, 4 | frontmatterSchema, 5 | metaSchema, 6 | } from 'fumadocs-mdx/config'; 7 | 8 | // You can customise Zod schemas for frontmatter and `meta.json` here 9 | // see https://fumadocs.dev/docs/mdx/collections#define-docs 10 | export const docs = defineDocs({ 11 | docs: { 12 | schema: frontmatterSchema, 13 | }, 14 | meta: { 15 | schema: metaSchema, 16 | }, 17 | }); 18 | 19 | export default defineConfig({ 20 | mdxOptions: { 21 | // MDX options 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files (only dist should be published) 2 | src/ 3 | *.ts 4 | *.tsx 5 | 6 | # Development files 7 | docs/ 8 | rollup.config.js 9 | tsconfig.json 10 | .eslintrc.json 11 | 12 | # Git 13 | .git/ 14 | .gitignore 15 | 16 | # Node 17 | node_modules/ 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # IDE 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | 28 | # OS 29 | .DS_Store 30 | Thumbs.db 31 | 32 | # Logs 33 | logs/ 34 | *.log 35 | 36 | # Testing 37 | coverage/ 38 | .nyc_output/ 39 | 40 | # Cache 41 | .eslintcache 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // Export all public color utilities for external use 2 | export { 3 | formatColorString, 4 | // Utility functions 5 | getContrastColor, 6 | hexToHsl, 7 | hexToHsv, 8 | // Hex conversions 9 | hexToRgb, 10 | hslToHex, 11 | hslToHsv, 12 | // HSL conversions 13 | hslToRgb, 14 | hsvToHex, 15 | hsvToHsl, 16 | // HSV conversions 17 | hsvToRgb, 18 | randomHex, 19 | // RGB conversions 20 | rgbToHex, 21 | rgbToHsl, 22 | rgbToHsv, 23 | } from './public'; 24 | 25 | // Re-export types for convenience 26 | export type { ColorInput, HslColor, HsvColor, RgbColor } from '../types'; 27 | -------------------------------------------------------------------------------- /docs/lib/layout.shared.tsx: -------------------------------------------------------------------------------- 1 | import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; 2 | import { Rainbow } from 'lucide-react'; 3 | 4 | /** 5 | * Shared layout configurations 6 | * 7 | * you can customise layouts individually from: 8 | * Home Layout: app/(home)/layout.tsx 9 | * Docs Layout: app/docs/layout.tsx 10 | */ 11 | export function baseOptions(): BaseLayoutProps { 12 | return { 13 | nav: { 14 | title: ( 15 | <> 16 | 17 | 18 | ), 19 | }, 20 | // see https://fumadocs.dev/docs/ui/navigation/links 21 | links: [], 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "es2018"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": false, 17 | "jsx": "react-jsx", 18 | "declaration": true, 19 | "outDir": "dist" 20 | }, 21 | "include": [ 22 | "src" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Pointer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from '../utils'; 3 | 4 | interface PointerProps { 5 | className?: string; 6 | top?: number; 7 | left: number; 8 | color: string; 9 | } 10 | 11 | export const Pointer: React.FC = ({ className, top = 0.5, left, color }) => { 12 | return ( 13 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/.source": ["./.source/index.ts"], 20 | "@/*": ["./*"] 21 | }, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --turbo", 8 | "start": "next start", 9 | "postinstall": "fumadocs-mdx" 10 | }, 11 | "dependencies": { 12 | "@vercel/og": "^0.8.5", 13 | "fumadocs-core": "15.7.3", 14 | "fumadocs-mdx": "11.8.1", 15 | "fumadocs-ui": "15.7.3", 16 | "lucide-react": "^0.542.0", 17 | "next": "15.5.9", 18 | "react": "^19.1.1", 19 | "react-beautiful-color": "^2.1.0", 20 | "react-dom": "^19.1.1" 21 | }, 22 | "devDependencies": { 23 | "@tailwindcss/postcss": "^4.1.12", 24 | "@types/mdx": "^2.0.13", 25 | "@types/node": "24.3.0", 26 | "@types/react": "^19.1.11", 27 | "@types/react-dom": "^19.1.7", 28 | "postcss": "^8.5.6", 29 | "tailwindcss": "^4.1.12", 30 | "typescript": "^5.9.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import { resolve } from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, 'src/index.ts'), 11 | name: 'ReactBeautifulColor', 12 | formats: ['es', 'cjs'], 13 | fileName: format => `index.${format === 'es' ? 'mjs' : 'js'}`, 14 | }, 15 | rollupOptions: { 16 | external: ['react', 'react-dom', 'lucide-react'], 17 | output: { 18 | globals: { 19 | react: 'React', 20 | 'react-dom': 'ReactDOM', 21 | 'lucide-react': 'LucideReact', 22 | }, 23 | }, 24 | }, 25 | sourcemap: true, 26 | emptyOutDir: true, 27 | }, 28 | css: { 29 | postcss: { 30 | plugins: [require('@tailwindcss/postcss')], 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Özer Gökalpsezer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { source } from '@/lib/source'; 2 | import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; 3 | import type { Metadata } from 'next'; 4 | import { notFound } from 'next/navigation'; 5 | import { createRelativeLink } from 'fumadocs-ui/mdx'; 6 | import { getMDXComponents } from '@/mdx-components'; 7 | 8 | export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { 9 | const params = await props.params; 10 | const page = source.getPage(params.slug); 11 | if (!page) notFound(); 12 | 13 | const MDXContent = page.data.body; 14 | 15 | return ( 16 | 20 | {page.data.title} 21 | {page.data.description} 22 | 23 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export async function generateStaticParams() { 35 | return source.generateParams(); 36 | } 37 | 38 | export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise { 39 | const params = await props.params; 40 | const page = source.getPage(params.slug); 41 | if (!page) notFound(); 42 | 43 | return { 44 | title: page.data.title, 45 | description: page.data.description, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp 4 | .pnp.js 5 | 6 | # Production builds 7 | /dist 8 | /build 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage/ 18 | *.lcov 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Dependency directories 24 | jspm_packages/ 25 | 26 | # TypeScript cache 27 | *.tsbuildinfo 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional eslint cache 33 | .eslintcache 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | # Output of 'npm pack' 39 | *.tgz 40 | 41 | # Yarn Integrity file 42 | .yarn-integrity 43 | 44 | # dotenv environment variables file 45 | .env 46 | .env.local 47 | .env.development.local 48 | .env.test.local 49 | .env.production.local 50 | 51 | # parcel-bundler cache (https://parceljs.org/) 52 | .cache 53 | .parcel-cache 54 | 55 | # Next.js build output 56 | .next 57 | 58 | # Nuxt.js build / generate output 59 | .nuxt 60 | dist 61 | 62 | # Gatsby files 63 | .cache/ 64 | public 65 | 66 | # Storybook build outputs 67 | .out 68 | .storybook-out 69 | 70 | # Temporary folders 71 | tmp/ 72 | temp/ 73 | 74 | # Logs 75 | logs 76 | *.log 77 | npm-debug.log* 78 | yarn-debug.log* 79 | yarn-error.log* 80 | lerna-debug.log* 81 | 82 | # OS generated files 83 | .DS_Store 84 | .DS_Store? 85 | ._* 86 | .Spotlight-V100 87 | .Trashes 88 | ehthumbs.db 89 | Thumbs.db 90 | 91 | # IDE 92 | .vscode/ 93 | .idea/ 94 | *.swp 95 | *.swo 96 | *~ 97 | 98 | # Local development 99 | .vercel 100 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | This is a Next.js application generated with 4 | [Create Fumadocs](https://github.com/fuma-nama/fumadocs). 5 | 6 | Run development server: 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | pnpm dev 12 | # or 13 | yarn dev 14 | ``` 15 | 16 | Open http://localhost:3000 with your browser to see the result. 17 | 18 | ## Explore 19 | 20 | In the project, you can see: 21 | 22 | - `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content. 23 | - `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep. 24 | 25 | | Route | Description | 26 | | ------------------------- | ------------------------------------------------------ | 27 | | `app/(home)` | The route group for your landing page and other pages. | 28 | | `app/docs` | The documentation layout and pages. | 29 | | `app/api/search/route.ts` | The Route Handler for search. | 30 | 31 | ### Fumadocs MDX 32 | 33 | A `source.config.ts` config file has been included, you can customise different options like frontmatter schema. 34 | 35 | Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details. 36 | 37 | ## Learn More 38 | 39 | To learn more about Next.js and Fumadocs, take a look at the following 40 | resources: 41 | 42 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js 43 | features and API. 44 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 45 | - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs 46 | -------------------------------------------------------------------------------- /docs/content/docs/components/alpha.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alpha 3 | description: A transparency slider component for controlling color opacity 4 | --- 5 | 6 | import { AlphaExample } from '../../../components/ColorPickerExample'; 7 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 8 | 9 | ## Features 10 | 11 | - **Transparency Control**: Alpha values from 0 (transparent) to 1 (opaque) 12 | - **Visual Feedback**: Checkered background pattern for transparency preview 13 | - **Color Preview**: Gradient from transparent to full color 14 | - **Precise Control**: Exact percentage values 15 | 16 | ## Basic Usage 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ```tsx 25 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 26 | 27 | export function AlphaExample() { 28 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 29 | 30 | return ( 31 |
32 |
33 |

Alpha Selector

34 | 39 | 40 | 41 |
42 |
Current Alpha: {Math.round(colorState.alpha * 100)}%
43 |
44 |
45 |
46 | ); 47 | } 48 | ``` 49 |
50 |
51 | 52 | ## Props 53 | 54 | | Prop | Type | Default | Description | 55 | |------|------|---------|-------------| 56 | | `className` | `string` | - | Additional CSS classes | 57 | 58 | 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to react-beautiful-color 2 | 3 | Thanks for your interest in contributing! 🎨 4 | 5 | ## 🚀 Quick Start 6 | 7 | 1. **Fork and clone:** 8 | 9 | ```bash 10 | git clone https://github.com/your-username/react-beautiful-color.git 11 | cd react-beautiful-color 12 | ``` 13 | 14 | 2. **Install dependencies:** 15 | 16 | ```bash 17 | bun install 18 | cd docs && bun install && cd .. 19 | ``` 20 | 21 | 3. **Start development:** 22 | 23 | ```bash 24 | bun run dev # Library in watch mode 25 | cd docs && bun run dev # Docs dev server 26 | ``` 27 | 28 | ## 🛠️ Development Commands 29 | 30 | ```bash 31 | bun run build # Build library 32 | bun run test # Run tests 33 | bun run lint # Lint & fix code 34 | bun run typecheck # TypeScript check 35 | ``` 36 | 37 | ## 📝 Contributing 38 | 39 | 1. **Create a branch:** `git checkout -b feature/my-feature` 40 | 2. **Make changes** following our code style 41 | 3. **Test:** `bun run test && bun run lint && bun run typecheck` 42 | 4. **Commit:** Use conventional commits (`feat:`, `fix:`, `docs:`) 43 | 5. **Push and create PR** 44 | 45 | ## 🎯 Code Style 46 | 47 | - Single quotes, 2 spaces, semicolons required 48 | - Use TypeScript for all components 49 | - Accept `className` prop for styling 50 | - Follow compound components pattern 51 | - Run `bun run lint` before submitting 52 | 53 | ## 📁 Key Files 54 | 55 | - `src/components/` - React components 56 | - `src/hooks/` - Custom hooks 57 | - `src/types.ts` - TypeScript types 58 | - `src/utils.ts` - Utility functions 59 | - `docs/` - Next.js documentation site 60 | 61 | ## 🐛 Issues & Features 62 | 63 | - Use GitHub issues for bugs and feature requests 64 | - Include reproduction steps for bugs 65 | - Provide clear use cases for features 66 | 67 | Thanks for contributing! 🌈 68 | -------------------------------------------------------------------------------- /docs/content/docs/components/hue.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hue 3 | description: A hue selection slider component with full spectrum color support 4 | --- 5 | 6 | import { HueExample } from '../../../components/ColorPickerExample'; 7 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 8 | 9 | ## Features 10 | 11 | - **Full Spectrum**: Complete 360° hue range 12 | - **Smooth Gradients**: Beautiful color transitions 13 | - **Precise Control**: Exact hue value selection 14 | 15 | ## Usage 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ```tsx 24 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 25 | 26 | export function HueExample() { 27 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 28 | 29 | return ( 30 |
31 |
32 |

Hue Selector

33 | 38 | 39 | 40 |
41 |
Current Hue: {Math.round(colorState.hsv.h)}°
42 |
43 | Color: {colorState.hex} 44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | ``` 51 |
52 |
53 | 54 | ## Props 55 | 56 | | Prop | Type | Default | Description | 57 | |------|------|---------|-------------| 58 | | `className` | `string` | - | Additional CSS classes | 59 | -------------------------------------------------------------------------------- /src/components/Hue.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { cn } from '../utils'; 3 | import { clamp, round } from '../utils/internal'; 4 | import { Interaction, Interactive } from './Interactive'; 5 | import { Pointer } from './Pointer'; 6 | 7 | interface HueProps { 8 | hue: number; 9 | onChange: (newHue: { h: number }, finishedUpdates: boolean) => void; 10 | className?: string; 11 | onFinishedUpdates: () => void; 12 | } 13 | 14 | export const Hue: React.FC = ({ hue, onChange, className, onFinishedUpdates }) => { 15 | const handleMove = useCallback( 16 | (interaction: Interaction) => { 17 | const h = round(clamp(360 * interaction.left, 0, 360)); 18 | onChange({ h }, false); 19 | }, 20 | [onChange] 21 | ); 22 | 23 | const handleKey = useCallback( 24 | (offset: Interaction) => { 25 | onChange( 26 | { 27 | h: clamp(hue + offset.left * 360, 0, 360), 28 | }, 29 | true 30 | ); 31 | }, 32 | [hue, onChange] 33 | ); 34 | 35 | return ( 36 |
37 | 47 |
53 | 54 | 60 | 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /docs/content/docs/components/eye-dropper.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: EyeDropper 3 | description: Pick colors from anywhere on the screen using the browser's native eye dropper tool 4 | --- 5 | 6 | import { EyeDropperExample } from '../../../components/ColorPickerExample'; 7 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 8 | 9 | ## Overview 10 | 11 | The `ColorPicker.EyeDropper` component lets users pick colors from anywhere on the screen using the browser's native eye dropper tool. 12 | 13 | ## Basic Usage 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ```tsx 22 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 23 | import { Pipette } from 'lucide-react'; 24 | 25 | export function EyeDropperExample() { 26 | const [{ colorInput, colorState }, setColor] = useColorState({ 27 | type: 'hex', 28 | value: '#ff6b9d' 29 | }); 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | {/* Show selected color */} 40 |
44 |
45 | ); 46 | } 47 | ``` 48 | 49 | 50 | 51 | ## Props 52 | 53 | | Prop | Type | Default | Description | 54 | |------|------|---------|-------------| 55 | | `className` | `string` | - | Additional CSS classes | 56 | | `children` | `React.ReactNode` | - | Icon or content | 57 | 58 | ## Browser Support 59 | 60 | Only renders when the browser supports the EyeDropper API (Chrome 95+, Edge 95+, Safari 17+). -------------------------------------------------------------------------------- /docs/content/docs/components/saturation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Saturation 3 | description: A 2D saturation and brightness picker with intuitive color selection 4 | --- 5 | 6 | import { SaturationExample } from '../../../components/ColorPickerExample'; 7 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 8 | 9 | ## Features 10 | 11 | - **2D Selection**: Horizontal saturation, vertical brightness control 12 | - **Visual Gradients**: Real-time color preview with smooth transitions 13 | - **Precise Control**: Exact percentage values for saturation and brightness 14 | - **Interactive Pointer**: Clear visual indicator of current selection 15 | 16 | ## Basic Usage 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ```tsx 25 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 26 | 27 | export function SaturationExample() { 28 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 29 | 30 | return ( 31 |
32 |
33 |

Saturation Selector

34 | 39 | 40 | 41 |
42 |
43 |
Current Saturation: {Math.round(colorState.hsv.s)}%
44 |
Current Brightness: {Math.round(colorState.hsv.v)}%
45 |
46 | Color: {colorState.hex} 47 |
48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | ``` 55 |
56 |
57 | 58 | ## Props 59 | 60 | | Prop | Type | Default | Description | 61 | |------|------|---------|-------------| 62 | | `className` | `string` | - | Additional CSS classes | 63 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import { FlatCompat } from '@eslint/eslintrc'; 5 | import js from '@eslint/js'; 6 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 7 | import tsParser from '@typescript-eslint/parser'; 8 | import prettier from 'eslint-plugin-prettier'; 9 | import react from 'eslint-plugin-react'; 10 | import reactHooks from 'eslint-plugin-react-hooks'; 11 | import globals from 'globals'; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | const compat = new FlatCompat({ 16 | baseDirectory: __dirname, 17 | recommendedConfig: js.configs.recommended, 18 | allConfig: js.configs.all, 19 | }); 20 | 21 | export default [ 22 | ...compat.extends('prettier', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended'), 23 | { 24 | plugins: { 25 | prettier, 26 | '@typescript-eslint': typescriptEslint, 27 | react, 28 | 'react-hooks': reactHooks, 29 | }, 30 | languageOptions: { 31 | globals: { 32 | ...globals.browser, 33 | ...globals.jest, 34 | JSX: 'readonly', 35 | React: 'readonly', 36 | }, 37 | parser: tsParser, 38 | }, 39 | rules: { 40 | 'import/no-anonymous-default-export': 'off', 41 | 'react/react-in-jsx-scope': 'off', 42 | 'react/display-name': 'off', 43 | 'react-hooks/exhaustive-deps': 'warn', 44 | 'react-hooks/rules-of-hooks': 'error', 45 | 'react/self-closing-comp': 'error', 46 | 'prettier/prettier': 'error', 47 | 'object-shorthand': 'error', 48 | quotes: [ 49 | 'error', 50 | 'single', 51 | { 52 | avoidEscape: true, 53 | }, 54 | ], 55 | 'react/jsx-curly-brace-presence': [ 56 | 'error', 57 | { 58 | props: 'never', 59 | children: 'never', 60 | }, 61 | ], 62 | }, 63 | }, 64 | { 65 | files: ['**/*.ts', '**/*.tsx'], 66 | rules: { 67 | '@typescript-eslint/no-unused-vars': 'warn', 68 | '@typescript-eslint/restrict-template-expressions': 'off', 69 | '@typescript-eslint/no-floating-promises': 'off', 70 | }, 71 | }, 72 | ]; 73 | -------------------------------------------------------------------------------- /src/components/Saturation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { HsvaColor } from '../types'; 3 | import { cn } from '../utils'; 4 | import { clamp, round } from '../utils/internal'; 5 | import { Interaction, Interactive } from './Interactive'; 6 | import { Pointer } from './Pointer'; 7 | 8 | interface SaturationProps { 9 | hsva: HsvaColor; 10 | onChange: (newColor: { s: number; v: number }, finishedUpdates: boolean) => void; 11 | className?: string; 12 | onFinishedUpdates: () => void; 13 | } 14 | 15 | export const Saturation: React.FC = ({ hsva, onChange, className, onFinishedUpdates }) => { 16 | const handleMove = useCallback( 17 | (interaction: Interaction) => { 18 | const s = round(clamp(interaction.left * 100, 0, 100)); 19 | const v = round(clamp(100 - interaction.top * 100, 0, 100)); 20 | 21 | onChange({ s, v }, false); 22 | }, 23 | [onChange] 24 | ); 25 | 26 | const handleKey = useCallback( 27 | (offset: Interaction) => { 28 | onChange( 29 | { 30 | s: clamp(hsva.s + offset.left * 100, 0, 100), 31 | v: clamp(hsva.v - offset.top * 100, 0, 100), 32 | }, 33 | true 34 | ); 35 | }, 36 | [hsva.s, hsva.v, onChange] 37 | ); 38 | 39 | const pureHue = `hsl(${hsva.h}, 100%, 50%)`; 40 | 41 | return ( 42 |
43 | 51 |
59 | 60 | 66 | 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/hooks/useColorState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import { Color, ColorFormat, ColorInput, UseColorStateArrayReturn } from '../types'; 3 | import { assertUnreachable } from '../utils/internal'; 4 | 5 | function toColorInput(color: Color, format: ColorFormat) { 6 | switch (format) { 7 | case 'hex': 8 | return { type: 'hex' as const, value: color.getHex() }; 9 | case 'rgb': 10 | return { type: 'rgb' as const, ...color.getRgb() }; 11 | case 'rgba': 12 | return { type: 'rgba' as const, ...color.getRgba() }; 13 | case 'hsl': 14 | return { type: 'hsl' as const, ...color.getHsl() }; 15 | case 'hsla': 16 | return { type: 'hsla' as const, ...color.getHsla() }; 17 | case 'hsv': 18 | return { type: 'hsv' as const, ...color.getHsv() }; 19 | case 'hsva': 20 | return { type: 'hsva' as const, ...color.getHsva() }; 21 | default: 22 | assertUnreachable(format); 23 | } 24 | } 25 | 26 | type Initializer = T | (() => T); 27 | 28 | export const useColorState = (initialColor: Initializer = { type: 'hex', value: '#ff6b9d' }): UseColorStateArrayReturn => { 29 | const [value, setValue] = useState<{ color: Color; type: ColorFormat }>(() => { 30 | const input = typeof initialColor === 'function' ? initialColor() : initialColor; 31 | return { color: new Color(input), type: input.type }; 32 | }); 33 | const colorInput = useMemo(() => toColorInput(value.color, value.type), [value.color, value.type]); 34 | const colorState = useMemo(() => { 35 | const hsva = value.color.getHsva(); 36 | return { 37 | hex: value.color.getHex(), 38 | rgb: value.color.getRgb(), 39 | rgba: value.color.getRgba(), 40 | hsl: value.color.getHsl(), 41 | hsla: value.color.getHsla(), 42 | hsv: value.color.getHsv(), 43 | hsva, 44 | alpha: hsva.a, 45 | }; 46 | }, [value.color]); 47 | 48 | const setColor = useCallback((newColor: ColorInput | Color) => { 49 | try { 50 | const newValue = 'type' in newColor ? new Color(newColor) : newColor; 51 | setValue(oldValue => ({ color: newValue, type: oldValue.type })); 52 | } catch (error) { 53 | console.warn('Invalid color input:', newColor, error); 54 | } 55 | }, []); 56 | 57 | return [ 58 | { 59 | colorInput, 60 | colorState, 61 | }, 62 | setColor, 63 | ]; 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-beautiful-color", 3 | "version": "2.1.0", 4 | "description": "Ultra-lightweight, beautiful color picker for React with compound components - minimal and extremely customizable", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "style": "dist/react-beautiful-color.css", 9 | "sideEffects": false, 10 | "files": [ 11 | "dist", 12 | "README.md" 13 | ], 14 | "scripts": { 15 | "build:js": "vite build", 16 | "build:types": "tsc --emitDeclarationOnly --outDir dist", 17 | "build": "npm run build:js && npm run build:types", 18 | "dev": "vite build --watch", 19 | "test": "jest", 20 | "lint": "eslint src", 21 | "typecheck": "tsc --noEmit", 22 | "prepublishOnly": "npm run typecheck && npm run lint && npm run build", 23 | "publish:patch": "npm version patch && npm publish", 24 | "publish:minor": "npm version minor && npm publish", 25 | "publish:major": "npm version major && npm publish" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "color-picker", 30 | "compound-components", 31 | "beautiful", 32 | "typescript", 33 | "lightweight", 34 | "customizable", 35 | "minimal", 36 | "color", 37 | "hex", 38 | "rgb", 39 | "hsl", 40 | "hsv" 41 | ], 42 | "author": "ozergokalpsezer", 43 | "license": "MIT", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/ddoemonn/react-beautiful-color" 47 | }, 48 | "homepage": "https://www.react-beautiful-color.dev", 49 | "peerDependencies": { 50 | "react": "^18.0.0 || ^19.0.0", 51 | "react-dom": "^18.0.0 || ^19.0.0" 52 | }, 53 | "devDependencies": { 54 | "@eslint/js": "^9.34.0", 55 | "@tailwindcss/cli": "^4.1.12", 56 | "@tailwindcss/postcss": "^4.1.12", 57 | "@types/node": "^24.3.0", 58 | "@types/react": "^18.2.0", 59 | "@types/react-dom": "^18.2.0", 60 | "@typescript-eslint/eslint-plugin": "^6.0.0", 61 | "@typescript-eslint/parser": "^6.0.0", 62 | "@vitejs/plugin-react": "^5.0.1", 63 | "eslint": "^8.45.0", 64 | "eslint-plugin-react": "^7.33.0", 65 | "eslint-plugin-react-hooks": "^4.6.0", 66 | "jest": "^29.6.0", 67 | "postcss": "^8.5.6", 68 | "tailwindcss": "^4.1.12", 69 | "typescript": "^5.1.0", 70 | "vite": "^7.1.3", 71 | "eslint-config-prettier": "^9.1.0", 72 | "eslint-plugin-prettier": "^5.1.3", 73 | "prettier": "3.3.3" 74 | }, 75 | "dependencies": { 76 | "clsx": "^2.1.1", 77 | "tailwind-merge": "^3.3.1", 78 | "prettier-plugin-tailwindcss": "^0.6.8" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/Alpha.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import { Color } from '../types'; 3 | import { cn } from '../utils'; 4 | import { clamp, round } from '../utils/internal'; 5 | import { Interaction, Interactive } from './Interactive'; 6 | import { Pointer } from './Pointer'; 7 | 8 | interface HsvaColor { 9 | h: number; 10 | s: number; 11 | v: number; 12 | a: number; 13 | } 14 | 15 | interface AlphaProps { 16 | hsva: HsvaColor; 17 | onChange: (newAlpha: { a: number }, finishedUpdates: boolean) => void; 18 | className?: string; 19 | onFinishedUpdates: () => void; 20 | } 21 | 22 | export const Alpha: React.FC = ({ hsva, onChange, className, onFinishedUpdates }) => { 23 | const handleMove = useCallback( 24 | (interaction: Interaction) => { 25 | onChange({ a: interaction.left }, false); 26 | }, 27 | [onChange] 28 | ); 29 | 30 | const handleKey = useCallback( 31 | (offset: Interaction) => { 32 | onChange( 33 | { 34 | a: clamp(hsva.a + offset.left, 0, 1), 35 | }, 36 | true 37 | ); 38 | }, 39 | [hsva.a, onChange] 40 | ); 41 | 42 | const colorFrom = useMemo(() => new Color({ type: 'hsva', h: hsva.h, s: hsva.s, v: hsva.v, a: 0 }).format('hsla'), [hsva.h, hsva.s, hsva.v]); 43 | const colorTo = useMemo(() => new Color({ type: 'hsva', h: hsva.h, s: hsva.s, v: hsva.v, a: 1 }).format('hsla'), [hsva.h, hsva.s, hsva.v]); 44 | 45 | return ( 46 |
47 | 58 |
\')', 63 | }} 64 | /> 65 | 66 |
72 | 73 | 79 | 80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /src/utils/internal.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); 5 | 6 | export const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max); 7 | 8 | export const round = (num: number): number => Math.round(num); 9 | 10 | export const isValidHex = (hex: string): boolean => { 11 | const h = hex.replace('#', ''); 12 | return /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(h); 13 | }; 14 | 15 | export const normalizeHex = (hex: string): string => { 16 | const h = hex.replace('#', ''); 17 | if (h.length === 3) { 18 | return `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`; 19 | } 20 | return `#${h}`; 21 | }; 22 | 23 | export const extractAlphaFromHex = (hex: string): number => { 24 | const h = hex.replace('#', ''); 25 | if (h.length === 8) { 26 | const alphaHex = h.substring(6, 8); 27 | return parseInt(alphaHex, 16) / 255; 28 | } 29 | return 1; 30 | }; 31 | 32 | export const stripAlphaFromHex = (hex: string): string => { 33 | const h = hex.replace('#', ''); 34 | if (h.length === 8) { 35 | return `#${h.substring(0, 6)}`; 36 | } 37 | return hex; 38 | }; 39 | 40 | export const parseColorString = (input: string): string => { 41 | const clean = input.trim(); 42 | 43 | if (clean.startsWith('#')) { 44 | if (isValidHex(clean)) { 45 | return stripAlphaFromHex(normalizeHex(clean)); 46 | } 47 | return '#ff0000'; 48 | } 49 | 50 | const rgbMatch = clean.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/); 51 | if (rgbMatch) { 52 | const r = parseInt(rgbMatch[1]); 53 | const g = parseInt(rgbMatch[2]); 54 | const b = parseInt(rgbMatch[3]); 55 | if (r <= 255 && g <= 255 && b <= 255) { 56 | return rgbToHex({ r, g, b }); 57 | } 58 | } 59 | 60 | const hslMatch = clean.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*[\d.]+)?\)/); 61 | if (hslMatch) { 62 | const h = parseInt(hslMatch[1]); 63 | const s = parseInt(hslMatch[2]); 64 | const l = parseInt(hslMatch[3]); 65 | if (h <= 360 && s <= 100 && l <= 100) { 66 | return hslToHex({ h, s, l }); 67 | } 68 | } 69 | 70 | const namedColors: Record = { 71 | red: '#ff0000', 72 | green: '#008000', 73 | blue: '#0000ff', 74 | white: '#ffffff', 75 | black: '#000000', 76 | yellow: '#ffff00', 77 | cyan: '#00ffff', 78 | magenta: '#ff00ff', 79 | gray: '#808080', 80 | orange: '#ffa500', 81 | purple: '#800080', 82 | pink: '#ffc0cb', 83 | }; 84 | 85 | const lowerCase = clean.toLowerCase(); 86 | if (namedColors[lowerCase]) { 87 | return namedColors[lowerCase]; 88 | } 89 | 90 | return '#ff0000'; 91 | }; 92 | 93 | // Import functions from public for internal use 94 | import { hslToHex, rgbToHex } from './public'; 95 | 96 | export function assertUnreachable(_x: never): never { 97 | throw new Error('You should never get here.'); 98 | } 99 | -------------------------------------------------------------------------------- /docs/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Install and set up react-beautiful-color in your React project 4 | --- 5 | 6 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 7 | 8 | ## Installation 9 | 10 | 11 | 12 | ```bash 13 | npm install react-beautiful-color 14 | ``` 15 | 16 | 17 | ```bash 18 | yarn add react-beautiful-color 19 | ``` 20 | 21 | 22 | ```bash 23 | pnpm add react-beautiful-color 24 | ``` 25 | 26 | 27 | ```bash 28 | bun add react-beautiful-color 29 | ``` 30 | 31 | 32 | 33 | ## Setup 34 | 35 | **Add CSS to your layout file:** 36 | 37 | ```tsx 38 | // app/layout.tsx (Next.js) or pages/_app.tsx or index.tsx 39 | import 'react-beautiful-color/dist/react-beautiful-color.css'; 40 | ``` 41 | 42 | ## Basic Usage 43 | 44 | 45 | 46 | ```tsx 47 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 48 | 49 | function App() { 50 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#3b82f6' }); 51 | 52 | return ( 53 | 54 | 55 | 56 |
57 | 58 | 59 |
60 | 61 | 62 |
63 |
64 |
65 | ); 66 | } 67 | ``` 68 |
69 | 70 | ```tsx 71 | import { useState } from 'react'; 72 | import { ColorPicker, Color } from 'react-beautiful-color'; 73 | 74 | function App() { 75 | const [color, setColor] = useState(new Color({ type: 'hex', value: '#3b82f6' })); 76 | 77 | return ( 78 | 79 | 80 | 81 |
82 | 83 | 84 |
85 | 86 | 87 |
88 |
89 |
90 | ); 91 | } 92 | ``` 93 | 94 |
95 |
96 | 97 | ## Key Features 98 | 99 | - **🧩 Compound Components** - Compose your own layout 100 | - **🎨 Beautiful Design** - Clean, modern UI 101 | - **⚡ Smart Hook** - Format preservation with instant conversions 102 | - **🛡️ Type-Safe API** - Full TypeScript support 103 | - **🪶 Lightweight** - Pure Tailwind CSS, no dependencies 104 | 105 | ## Next Steps 106 | 107 | - Learn about [ColorPicker](/docs/components/color-picker) component 108 | - Explore [useColorState](/docs/hooks/use-color-state) hook 109 | - Check individual components: [Saturation](/docs/components/saturation), [Hue](/docs/components/hue), [Alpha](/docs/components/alpha) 110 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { convertColor, formatColorString, getContrastColor } from './utils'; 2 | 3 | export interface RgbColor { 4 | r: number; 5 | g: number; 6 | b: number; 7 | } 8 | 9 | export interface RgbaColor extends RgbColor { 10 | a: number; 11 | } 12 | 13 | export interface HslColor { 14 | h: number; 15 | s: number; 16 | l: number; 17 | } 18 | 19 | export interface HslaColor extends HslColor { 20 | a: number; 21 | } 22 | 23 | export interface HsvColor { 24 | h: number; 25 | s: number; 26 | v: number; 27 | } 28 | 29 | export interface HsvaColor extends HsvColor { 30 | a: number; 31 | } 32 | 33 | export type HexColor = string; 34 | 35 | export type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hsv' | 'hsva'; 36 | 37 | export interface ColorState { 38 | hex: string; 39 | rgb: RgbColor; 40 | rgba: RgbaColor; 41 | hsl: HslColor; 42 | hsla: HslaColor; 43 | hsv: HsvColor; 44 | hsva: HsvaColor; 45 | alpha: number; 46 | } 47 | 48 | // Type-safe color input objects using discriminated unions 49 | export type ColorInput = 50 | | { type: 'hex'; value: string } 51 | | { type: 'rgb'; r: number; g: number; b: number } 52 | | { type: 'rgba'; r: number; g: number; b: number; a: number } 53 | | { type: 'hsl'; h: number; s: number; l: number } 54 | | { type: 'hsla'; h: number; s: number; l: number; a: number } 55 | | { type: 'hsv'; h: number; s: number; v: number } 56 | | { type: 'hsva'; h: number; s: number; v: number; a: number }; 57 | 58 | export interface UseColorStateReturn { 59 | color: ColorState; 60 | setColor: (color: ColorInput) => void; 61 | setAlpha: (alpha: number) => void; 62 | setFromRgb: (rgb: RgbColor) => void; 63 | setFromHsl: (hsl: HslColor) => void; 64 | setFromHsv: (hsv: HsvColor) => void; 65 | } 66 | 67 | // New array-style return type 68 | export type UseColorStateArrayReturn = [ 69 | { 70 | colorInput: ColorInput; 71 | colorState: ColorState; 72 | }, 73 | (color: ColorInput | Color) => void, 74 | ]; 75 | 76 | export interface ColorPickerProps { 77 | defaultColor?: Color | ColorInput; 78 | color?: Color | ColorInput; 79 | onChange?: (color: Color) => void; 80 | className?: string; 81 | withEyeDropper?: boolean; 82 | } 83 | 84 | export class Color { 85 | private color: ColorInput; 86 | constructor(private colorInput: ColorInput | RgbaColor | RgbColor | HslColor | HslaColor | HsvColor | HsvaColor | string) { 87 | if (typeof colorInput === 'string') { 88 | this.color = { type: 'hex', value: colorInput }; 89 | } else if ('type' in colorInput) { 90 | this.color = colorInput; 91 | } else if ('r' in colorInput) { 92 | if ('a' in colorInput) { 93 | this.color = { type: 'rgba', ...colorInput }; 94 | } else { 95 | this.color = { type: 'rgb', ...colorInput }; 96 | } 97 | } else if ('v' in colorInput) { 98 | if ('a' in colorInput) { 99 | this.color = { type: 'hsva', ...colorInput }; 100 | } else { 101 | this.color = { type: 'hsv', ...colorInput }; 102 | } 103 | } else { 104 | if ('a' in colorInput) { 105 | this.color = { type: 'hsla', ...colorInput }; 106 | } else { 107 | this.color = { type: 'hsl', ...colorInput }; 108 | } 109 | } 110 | } 111 | public getRgb(): RgbColor { 112 | return convertColor(this.color, 'rgb'); 113 | } 114 | public getHsv(): HsvColor { 115 | return convertColor(this.color, 'hsv'); 116 | } 117 | public getHsla(): HslaColor { 118 | return convertColor(this.color, 'hsla'); 119 | } 120 | public getHsva(): HsvaColor { 121 | return convertColor(this.color, 'hsva'); 122 | } 123 | public getHex(): HexColor { 124 | return convertColor(this.color, 'hex'); 125 | } 126 | public getHsl(): HslColor { 127 | return convertColor(this.color, 'hsl'); 128 | } 129 | public getRgba(): RgbaColor { 130 | return convertColor(this.color, 'rgba'); 131 | } 132 | public format(format: Exclude = 'hex'): string { 133 | return formatColorString(this, format); 134 | } 135 | public getContrastingColor(): Color { 136 | return new Color({ type: 'hex', value: getContrastColor(this.getHex()) }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/components/Interactive.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef } from 'react'; 2 | 3 | import { cn } from '../utils'; 4 | import { clamp } from '../utils/internal'; 5 | 6 | export interface Interaction { 7 | left: number; 8 | top: number; 9 | } 10 | 11 | interface InteractiveProps { 12 | onMove: (interaction: Interaction) => void; 13 | onMoveEnd: () => void; 14 | onKey?: (offset: Interaction) => void; 15 | children: React.ReactNode; 16 | className?: string; 17 | 'aria-label'?: string; 18 | 'aria-valuenow'?: number; 19 | 'aria-valuemax'?: number; 20 | 'aria-valuemin'?: number; 21 | 'aria-valuetext'?: string; 22 | } 23 | 24 | const getRelativePosition = (element: HTMLElement, event: MouseEvent | TouchEvent): Interaction => { 25 | const rect = element.getBoundingClientRect(); 26 | 27 | // Get pointer coordinates 28 | let clientX: number; 29 | let clientY: number; 30 | 31 | if ('touches' in event) { 32 | // Touch event 33 | const touch = event.touches[0] || event.changedTouches[0]; 34 | clientX = touch.clientX; 35 | clientY = touch.clientY; 36 | } else { 37 | // Mouse event 38 | clientX = event.clientX; 39 | clientY = event.clientY; 40 | } 41 | 42 | // Calculate relative position 43 | const x = clientX - rect.left; 44 | const y = clientY - rect.top; 45 | 46 | // Convert to normalized coordinates (0-1) 47 | const left = clamp(x / rect.width, 0, 1); 48 | const top = clamp(y / rect.height, 0, 1); 49 | 50 | return { left, top }; 51 | }; 52 | 53 | export const Interactive: React.FC = ({ onMove, onMoveEnd, onKey, children, className, ...ariaProps }) => { 54 | const container = useRef(null); 55 | const isPressed = useRef(false); 56 | 57 | const handleMove = useCallback( 58 | (event: MouseEvent | TouchEvent) => { 59 | if (!isPressed.current || !container.current) return; 60 | 61 | event.preventDefault(); 62 | onMove(getRelativePosition(container.current, event)); 63 | }, 64 | [onMove] 65 | ); 66 | 67 | const handleMoveEnd = useCallback(() => { 68 | isPressed.current = false; 69 | 70 | onMoveEnd(); 71 | document.removeEventListener('mousemove', handleMove); 72 | document.removeEventListener('mouseup', handleMoveEnd); 73 | document.removeEventListener('touchmove', handleMove); 74 | document.removeEventListener('touchend', handleMoveEnd); 75 | }, [handleMove, onMoveEnd]); 76 | 77 | const handleMoveStart = useCallback( 78 | (event: React.MouseEvent | React.TouchEvent) => { 79 | const element = container.current; 80 | if (!element) return; 81 | 82 | event.preventDefault(); 83 | 84 | isPressed.current = true; 85 | 86 | // Update position immediately 87 | onMove(getRelativePosition(element, event.nativeEvent)); 88 | 89 | // Add event listeners 90 | document.addEventListener('mousemove', handleMove); 91 | document.addEventListener('mouseup', handleMoveEnd); 92 | document.addEventListener('touchmove', handleMove, { passive: false }); 93 | document.addEventListener('touchend', handleMoveEnd); 94 | }, 95 | [onMove, handleMove, handleMoveEnd] 96 | ); 97 | 98 | const handleKeyDown = useCallback( 99 | (event: React.KeyboardEvent) => { 100 | if (!onKey) return; 101 | 102 | const keyCode = event.which || event.keyCode; 103 | 104 | if (keyCode < 37 || keyCode > 40) return; 105 | 106 | event.preventDefault(); 107 | 108 | onKey({ 109 | left: keyCode === 39 ? 0.05 : keyCode === 37 ? -0.05 : 0, 110 | top: keyCode === 40 ? 0.05 : keyCode === 38 ? -0.05 : 0, 111 | }); 112 | }, 113 | [onKey] 114 | ); 115 | 116 | // Clean up event listeners on unmount 117 | useEffect(() => { 118 | return () => { 119 | document.removeEventListener('mousemove', handleMove); 120 | document.removeEventListener('mouseup', handleMoveEnd); 121 | document.removeEventListener('touchmove', handleMove); 122 | document.removeEventListener('touchend', handleMoveEnd); 123 | }; 124 | }, [handleMove, handleMoveEnd]); 125 | 126 | return ( 127 |
137 | {children} 138 |
139 | ); 140 | }; 141 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ozergokalpsezer@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import 'react-beautiful-color/dist/react-beautiful-color.css'; 2 | 3 | import '@/app/global.css'; 4 | 5 | import { RootProvider } from 'fumadocs-ui/provider'; 6 | import { Geist, Geist_Mono, Inter, JetBrains_Mono, Orbitron, Space_Grotesk, Pacifico } from 'next/font/google'; 7 | import type { Metadata, Viewport } from 'next'; 8 | 9 | const geistSans = Geist({ 10 | variable: '--font-geist-sans', 11 | subsets: ['latin'], 12 | }); 13 | 14 | const geistMono = Geist_Mono({ 15 | variable: '--font-geist-mono', 16 | subsets: ['latin'], 17 | }); 18 | 19 | const spaceGrotesk = Space_Grotesk({ 20 | variable: '--font-space-grotesk', 21 | subsets: ['latin'], 22 | weight: ['400', '500', '600', '700'], 23 | }); 24 | 25 | const inter = Inter({ 26 | variable: '--font-inter', 27 | subsets: ['latin'], 28 | weight: ['900'], 29 | }); 30 | 31 | const jetbrains = JetBrains_Mono({ 32 | variable: '--font-jetbrains', 33 | subsets: ['latin'], 34 | weight: ['800'], 35 | }); 36 | 37 | const orbitron = Orbitron({ 38 | variable: '--font-orbitron', 39 | subsets: ['latin'], 40 | weight: ['900'], 41 | }); 42 | 43 | const pacifico = Pacifico({ 44 | variable: '--font-pacifico', 45 | subsets: ['latin'], 46 | weight: ['400'], 47 | }); 48 | 49 | export const metadata: Metadata = { 50 | title: { 51 | default: 'React Beautiful Color - Flexible Color Picker for React', 52 | template: '%s | React Beautiful Color', 53 | }, 54 | description: 55 | 'The most flexible and beautiful color picker for React. Built with compound components for maximum customization. Features TypeScript support, eye dropper, and all color formats.', 56 | keywords: [ 57 | 'react', 58 | 'color picker', 59 | 'react color picker', 60 | 'react color picker component', 61 | 'react color picker library', 62 | 'react color picker package', 63 | 'react color picker npm', 64 | 'react color picker yarn', 65 | 'react color picker pnpm', 66 | 'react color picker bun', 67 | 'color selector', 68 | 'react component', 69 | 'typescript', 70 | 'tailwind css', 71 | 'compound components', 72 | 'eye dropper', 73 | 'hex', 74 | 'rgb', 75 | 'hsl', 76 | 'hsv', 77 | 'color formats', 78 | 'react-beautiful-color', 79 | ], 80 | authors: [ 81 | { 82 | name: 'Özer Gökalpsezer', 83 | url: 'https://github.com/ddoemonn', 84 | }, 85 | ], 86 | creator: 'Özer Gökalpsezer', 87 | publisher: 'Özer Gökalpsezer', 88 | formatDetection: { 89 | email: false, 90 | address: false, 91 | telephone: false, 92 | }, 93 | metadataBase: new URL('https://react-beautiful-color.vercel.app'), 94 | alternates: { 95 | canonical: '/', 96 | }, 97 | robots: { 98 | index: true, 99 | follow: true, 100 | googleBot: { 101 | index: true, 102 | follow: true, 103 | 'max-video-preview': -1, 104 | 'max-image-preview': 'large', 105 | 'max-snippet': -1, 106 | }, 107 | }, 108 | category: 'technology', 109 | }; 110 | 111 | export const viewport: Viewport = { 112 | themeColor: [ 113 | { media: '(prefers-color-scheme: light)', color: 'white' }, 114 | { media: '(prefers-color-scheme: dark)', color: 'black' }, 115 | ], 116 | width: 'device-width', 117 | initialScale: 1, 118 | maximumScale: 5, 119 | }; 120 | 121 | export default function Layout({ children }: LayoutProps<'/'>) { 122 | return ( 123 | 128 | 129 | React Beautiful Color - Flexible Color Picker for React 130 | 134 | 135 | 139 | 143 | 147 | 151 | 155 | 156 | 160 | 164 | 168 | 172 | 176 | 180 | 181 | 182 | {children} 183 | 184 | 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /src/components/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; 2 | 3 | import type { ColorInput, ColorPickerProps, HsvaColor } from '../types'; 4 | import { Color } from '../types'; 5 | import { cn } from '../utils'; 6 | 7 | import { convertColor } from '../utils'; 8 | import { Alpha } from './Alpha'; 9 | import { Hue } from './Hue'; 10 | import { Saturation } from './Saturation'; 11 | 12 | interface ColorPickerContextType { 13 | hsva: HsvaColor; 14 | updateHsva: (params: Partial, finishedUpdates: boolean) => void; 15 | handleEyeDropper: () => void; 16 | onFinishedUpdates: () => void; 17 | } 18 | 19 | const ColorPickerContext = createContext(null); 20 | 21 | const useColorPickerContext = () => { 22 | const context = useContext(ColorPickerContext); 23 | if (!context) { 24 | throw new Error('ColorPicker compound components must be used within a ColorPicker'); 25 | } 26 | return context; 27 | }; 28 | 29 | interface ColorPickerMainProps extends Omit { 30 | children?: React.ReactNode; 31 | } 32 | 33 | function getColor(color: Color | ColorInput): Color { 34 | if ('type' in color) { 35 | return new Color(color); 36 | } 37 | return color; 38 | } 39 | 40 | const ColorPickerMain = ({ color, onChange, className, children, defaultColor, ...rest }: ColorPickerMainProps) => { 41 | const [localColor, setLocalColor] = useState(getColor(color ?? defaultColor ?? { h: 340, s: 58, v: 100, a: 1, type: 'hsva' })); 42 | const updating = useRef(false); 43 | const colorRef = useRef(localColor); 44 | 45 | useEffect(() => { 46 | if (!updating.current && color) { 47 | colorRef.current = getColor(color); 48 | setLocalColor(colorRef.current); 49 | } 50 | }, [color]); 51 | 52 | const updateHsva = useCallback( 53 | (hsvaColor: Partial, finishedUpdates: boolean) => { 54 | const newColor = new Color({ type: 'hsva', ...colorRef.current.getHsva(), ...hsvaColor }); 55 | colorRef.current = newColor; 56 | setLocalColor(newColor); 57 | updating.current = !finishedUpdates; 58 | onChange?.(newColor); 59 | }, 60 | [onChange] 61 | ); 62 | 63 | const onFinishedUpdates = useCallback(() => { 64 | updating.current = false; 65 | }, []); 66 | 67 | const handleEyeDropper = useCallback(async () => { 68 | if (!('EyeDropper' in window)) { 69 | console.log('EyeDropper API not supported'); 70 | return; 71 | } 72 | 73 | try { 74 | // @ts-expect-error EyeDropper API not in TypeScript types yet 75 | const eyeDropper = new window.EyeDropper(); 76 | const result = await eyeDropper.open(); 77 | if (result.sRGBHex) { 78 | updateHsva(convertColor({ type: 'hex', value: result.sRGBHex }, 'hsva'), true); 79 | } 80 | } catch (error) { 81 | console.log('EyeDropper cancelled or failed:', error); 82 | } 83 | }, [onChange]); 84 | 85 | const contextValue: ColorPickerContextType = { 86 | hsva: localColor.getHsva(), 87 | updateHsva, 88 | handleEyeDropper, 89 | onFinishedUpdates, 90 | }; 91 | 92 | return ( 93 | 94 |
98 | {children} 99 |
100 |
101 | ); 102 | }; 103 | 104 | interface CompoundSaturationProps { 105 | className?: string; 106 | } 107 | 108 | const CompoundSaturation = ({ className }: CompoundSaturationProps) => { 109 | const { hsva, updateHsva, onFinishedUpdates } = useColorPickerContext(); 110 | 111 | const handleSaturationChange = useCallback( 112 | (newColor: { s: number; v: number }, finishedUpdates: boolean) => { 113 | updateHsva(newColor, finishedUpdates); 114 | }, 115 | [updateHsva] 116 | ); 117 | 118 | return ( 119 | 125 | ); 126 | }; 127 | 128 | interface CompoundHueProps { 129 | className?: string; 130 | } 131 | 132 | const CompoundHue = ({ className }: CompoundHueProps) => { 133 | const { hsva, updateHsva, onFinishedUpdates } = useColorPickerContext(); 134 | 135 | const handleHueChange = useCallback( 136 | (newHue: { h: number }, finishedUpdates: boolean) => { 137 | updateHsva(newHue, finishedUpdates); 138 | }, 139 | [updateHsva] 140 | ); 141 | 142 | return ( 143 | 149 | ); 150 | }; 151 | 152 | interface CompoundAlphaProps { 153 | className?: string; 154 | } 155 | 156 | const CompoundAlpha = ({ className }: CompoundAlphaProps) => { 157 | const { hsva, updateHsva, onFinishedUpdates } = useColorPickerContext(); 158 | 159 | const handleAlphaChange = useCallback( 160 | (newAlpha: { a: number }, finishedUpdates: boolean) => { 161 | updateHsva(newAlpha, finishedUpdates); 162 | }, 163 | [updateHsva] 164 | ); 165 | 166 | return ( 167 | 173 | ); 174 | }; 175 | 176 | interface CompoundEyeDropperProps { 177 | className?: string; 178 | size?: number; 179 | title?: string; 180 | children?: React.ReactNode; 181 | } 182 | 183 | const CompoundEyeDropper = ({ className, title = 'Pick color from screen', children }: CompoundEyeDropperProps) => { 184 | const { handleEyeDropper } = useColorPickerContext(); 185 | 186 | const [supported, setSupported] = useState(false); 187 | useEffect(() => { 188 | if (typeof window !== 'undefined' && 'EyeDropper' in window) { 189 | setSupported(true); 190 | } 191 | }, []); 192 | 193 | if (!supported) { 194 | return null; 195 | } 196 | return ( 197 | 204 | ); 205 | }; 206 | 207 | export const ColorPicker = Object.assign(ColorPickerMain, { 208 | Saturation: CompoundSaturation, 209 | Hue: CompoundHue, 210 | Alpha: CompoundAlpha, 211 | EyeDropper: CompoundEyeDropper, 212 | }); 213 | -------------------------------------------------------------------------------- /docs/content/docs/hooks/use-color-state.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: useColorState 3 | description: A powerful React hook for managing color state with automatic format conversion and format preservation 4 | --- 5 | 6 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 7 | 8 | ## Features 9 | 10 | - **Multi-Format Support**: Handles hex, rgb, rgba, hsl, hsla, hsv, hsva 11 | - **Format Preservation**: Maintains your original input format throughout color changes 12 | - **Automatic Conversion**: All formats (hex, rgb, hsl, hsv, alpha) available instantly via colorState 13 | - **Type Safety**: Full TypeScript support with type-safe inputs 14 | - **Alpha Channel**: Built-in transparency support 15 | 16 | ## Basic Usage 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Color Preview

24 |
28 |
29 |
30 |

Color Values

31 |
32 |
Hex: #ff6b9d
33 |
RGB: 255, 107, 157
34 |
HSL: 334°, 100%, 71%
35 |
Alpha: 100%
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | ```tsx 44 | import { useColorState } from 'react-beautiful-color'; 45 | 46 | function ColorDemo() { 47 | const [{ colorInput, colorState }, setColor] = useColorState({ 48 | type: 'hex', 49 | value: '#ff6b9d' 50 | }); 51 | 52 | return ( 53 |
54 |
58 | 59 |
60 |
Input Format: {colorInput.type}
61 |
Hex: {colorState.hex}
62 |
RGB: {colorState.rgb.r}, {colorState.rgb.g}, {colorState.rgb.b}
63 |
HSL: {colorState.hsl.h}°, {colorState.hsl.s}%, {colorState.hsl.l}%
64 |
Alpha: {Math.round(colorState.alpha * 100)}%
65 |
66 |
67 | ); 68 | } 69 | ``` 70 | 71 | 72 | 73 | ## API Reference 74 | 75 | ### Parameters 76 | 77 | | Parameter | Type | Default | Description | 78 | |-----------|------|---------|-------------| 79 | | `initialColor` | `ColorInput` | `{ type: 'hex', value: '#ff6b9d' }` | Initial color value | 80 | 81 | ### Return Value 82 | 83 | Returns an array with two elements: 84 | 85 | ```tsx 86 | [{ colorInput, colorState }, setColor] 87 | ``` 88 | 89 | | Index | Property | Type | Description | 90 | |-------|----------|------|-------------| 91 | | `[0]` | `colorInput` | `ColorInput` | Current color in the original format you specified | 92 | | `[0]` | `colorState` | `ColorState` | Current color in all formats - see ColorState Properties below | 93 | | `[1]` | `setColor` | `(color: ColorInput \| Color) => void` | Update color from any format | 94 | 95 | ### ColorState - Access Every Color Format 96 | 97 | The `colorState` object contains the current color in **all supported formats**. You can access any color representation instantly: 98 | 99 | - **`colorState.hex`** - Hex string like `"#ff6b9d"` 100 | - **`colorState.rgb`** - RGB object with `r`, `g`, `b` values (0-255) 101 | - **`colorState.rgba`** - RGBA object with `r`, `g`, `b`, `a` values 102 | - **`colorState.hsl`** - HSL object with `h` (0-360°), `s`, `l` (0-100%) 103 | - **`colorState.hsla`** - HSLA object with `h`, `s`, `l`, `a` values 104 | - **`colorState.hsv`** - HSV object with `h` (0-360°), `s`, `v` (0-100%) 105 | - **`colorState.hsva`** - HSVA object with `h`, `s`, `v`, `a` values 106 | - **`colorState.alpha`** - Alpha value (0-1) 107 | 108 | **Example:** 109 | 110 | ```tsx 111 | const [{ colorInput, colorState }, setColor] = useColorState({ 112 | type: 'hex', 113 | value: '#ff6b9d' 114 | }); 115 | 116 | // Access any format instantly - no conversion needed! 117 | console.log(colorState.hex); // "#ff6b9d" 118 | console.log(colorState.rgb.r); // 255 119 | console.log(colorState.rgba.a); // 1.0 120 | console.log(colorState.hsl.h); // 334 121 | console.log(colorState.hsla.a); // 1.0 122 | console.log(colorState.hsv.v); // 100 123 | console.log(colorState.hsva.a); // 1.0 124 | console.log(colorState.alpha); // 1.0 125 | 126 | // Use directly in CSS 127 |
132 | ``` 133 | 134 | ## Format Preservation 135 | 136 | One of the key features is that `colorInput` always maintains your original format: 137 | 138 | ```tsx 139 | // Initialize with HSVA 140 | const [{ colorInput, colorState }, setColor] = useColorState({ 141 | type: 'hsva', 142 | h: 334, 143 | s: 100, 144 | v: 100, 145 | a: 0.5 146 | }); 147 | 148 | // Even after changing colors, colorInput stays HSVA format 149 | setColor({ type: 'hex', value: '#ff0000' }); 150 | 151 | console.log(colorInput); 152 | // Still HSVA: { type: 'hsva', h: 0, s: 100, v: 100, a: 0.5 } 153 | 154 | console.log(colorState.hex); 155 | // "#ff0000" 156 | ``` 157 | 158 | ## Color Formats 159 | 160 | 161 | 162 | ```tsx 163 | setColor({ type: 'hex', value: '#ff6b9d' }); 164 | ``` 165 | 166 | 167 | 168 | ```tsx 169 | // RGB 170 | setColor({ type: 'rgb', r: 255, g: 107, b: 157 }); 171 | 172 | // RGBA (with alpha) 173 | setColor({ type: 'rgba', r: 255, g: 107, b: 157, a: 0.8 }); 174 | ``` 175 | 176 | 177 | 178 | ```tsx 179 | // HSL 180 | setColor({ type: 'hsl', h: 334, s: 100, l: 71 }); 181 | 182 | // HSLA (with alpha) 183 | setColor({ type: 'hsla', h: 334, s: 100, l: 71, a: 0.8 }); 184 | ``` 185 | 186 | 187 | 188 | ```tsx 189 | // HSV 190 | setColor({ type: 'hsv', h: 334, s: 58, v: 100 }); 191 | 192 | // HSVA (with alpha) 193 | setColor({ type: 'hsva', h: 334, s: 58, v: 100, a: 0.8 }); 194 | ``` 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-beautiful-color 2 | 3 | [![Image](https://i.hizliresim.com/5o05ik8.png)](https://hizliresim.com/5o05ik8) 4 | 5 | The most flexible and beautiful color picker for React. Built with compound components for maximum customization. 6 | 7 | ## Why Choose This Over Others? 8 | 9 | - **🧩 Compound Components** - Compose your own layout, unlike rigid alternatives 10 | - **🎨 Beautiful Design** - Clean, modern UI that fits any design system 11 | - **⚡ Smart Hook** - `useColorState` preserves your input format while providing all color formats instantly 12 | - **🛡️ Type-Safe API** - Full TypeScript support with comprehensive type definitions 13 | - **👁️ Eye Dropper Support** - Built-in screen color picker (where browser supports it) 14 | - **🎯 Format Preservation** - Maintains your original color format throughout interactions 15 | - **🌈 Universal Format Support** - Hex, RGB/RGBA, HSL/HSLA, HSV/HSVA with alpha channel 16 | - **🪶 Lightweight** - Pure Tailwind CSS, no external dependencies 17 | - **🛠️ Fully Customizable** - Style and arrange components however you want 18 | - **🔧 Rich Utilities** - Comprehensive color conversion and manipulation utilities 19 | 20 | ## Installation 21 | 22 | ```bash 23 | bun add react-beautiful-color 24 | ``` 25 | 26 | 📖 **[View Full Documentation →](https://react-beautiful-color.vercel.app)** 27 | 28 | ## Quick Start 29 | 30 | **1. Add CSS to your layout file:** 31 | 32 | ```tsx 33 | // app/layout.tsx (Next.js) or pages/_app.tsx or index.tsx 34 | import 'react-beautiful-color/dist/react-beautiful-color.css'; 35 | ``` 36 | 37 | **2. Use the component:** 38 | 39 | ```tsx 40 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 41 | import { Pipette } from 'lucide-react'; 42 | 43 | function App() { 44 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#3b82f6' }); 45 | 46 | return ( 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 |
59 |
60 |
61 | ); 62 | } 63 | ``` 64 | 65 | ## Components 66 | 67 | ### 🎨 ColorPicker 68 | 69 | Compose your own layout with these sub-components: 70 | 71 | - **`ColorPicker.Saturation`** - Saturation and brightness selection area 72 | - **`ColorPicker.Hue`** - Hue selection slider 73 | - **`ColorPicker.Alpha`** - Alpha/transparency slider 74 | - **`ColorPicker.EyeDropper`** - Eye dropper tool (browser-dependent) 75 | 76 | [📖 Learn more about ColorPicker →](https://react-beautiful-color.vercel.app/docs/components/color-picker) 77 | 78 | ### ⚡ useColorState Hook 79 | 80 | Intelligent state management with format preservation: 81 | 82 | ```tsx 83 | const [{ colorInput, colorState }, setColor] = useColorState({ 84 | type: 'hsva', 85 | h: 334, s: 100, v: 100, a: 0.5 86 | }); 87 | 88 | // colorInput preserves your format - always HSVA! 89 | console.log(colorInput); // { type: 'hsva', h: 334, s: 100, v: 100, a: 0.5 } 90 | 91 | // colorState provides ALL formats instantly 92 | console.log(colorState.hex); // "#ff6b9d" 93 | console.log(colorState.rgb); // { r: 255, g: 107, b: 157 } 94 | console.log(colorState.hsl); // { h: 334, s: 100, l: 71 } 95 | console.log(colorState.alpha); // 0.5 96 | ``` 97 | 98 | [📖 Learn more about useColorState →](https://react-beautiful-color.vercel.app/docs/hooks/use-color-state) 99 | 100 | ## Utilities 101 | 102 | Powerful color conversion and manipulation utilities: 103 | 104 | ```tsx 105 | import { hexToRgb, rgbToHex, hexToHsl, hslToHex } from 'react-beautiful-color'; 106 | 107 | // Color conversions 108 | const rgb = hexToRgb('#ff6b9d'); // { r: 255, g: 107, b: 157 } 109 | const hex = rgbToHex(255, 107, 157); // "#ff6b9d" 110 | const hsl = hexToHsl('#ff6b9d'); // { h: 334, s: 100, l: 71 } 111 | ``` 112 | 113 | [📖 View all utility functions →](https://react-beautiful-color.vercel.app/docs/utils) 114 | 115 | ## Advanced Usage 116 | 117 | ### Multiple Format Support 118 | 119 | Set colors in any format with complete type safety: 120 | 121 | ```tsx 122 | setColor({ type: 'hex', value: '#ff0000' }); 123 | setColor({ type: 'rgb', r: 255, g: 0, b: 0 }); 124 | setColor({ type: 'hsl', h: 0, s: 100, l: 50 }); 125 | setColor({ type: 'rgba', r: 255, g: 0, b: 0, a: 0.5 }); 126 | setColor({ type: 'hsla', h: 0, s: 100, l: 50, a: 0.8 }); 127 | setColor({ type: 'hsva', h: 0, s: 100, v: 100, a: 0.9 }); 128 | ``` 129 | 130 | ### Alternative without Hook 131 | 132 | Use the `Color` class directly for more control: 133 | 134 | ```tsx 135 | import { useState } from 'react'; 136 | import { ColorPicker, Color } from 'react-beautiful-color'; 137 | 138 | function App() { 139 | const [color, setColor] = useState(new Color({ type: 'hex', value: '#3b82f6' })); 140 | 141 | // Access color properties 142 | const rgba = color.getRgba(); 143 | const hex = color.getHex(); 144 | const hsl = color.getHsl(); 145 | 146 | return ( 147 | 148 | 149 | 150 |
151 | 152 | 153 |
154 | 155 | 156 |
157 |
158 |
159 | ); 160 | } 161 | ``` 162 | 163 | ## Documentation 164 | 165 | 📖 **[View Full Documentation →](https://react-beautiful-color.vercel.app)** 166 | 167 | - **[Getting Started](https://react-beautiful-color.vercel.app/docs)** - Installation and basic setup 168 | - **[ColorPicker Component](https://react-beautiful-color.vercel.app/docs/components/color-picker)** - Main component API 169 | - **[useColorState Hook](https://react-beautiful-color.vercel.app/docs/hooks/use-color-state)** - Intelligent state management 170 | - **[Individual Components](https://react-beautiful-color.vercel.app/docs/components/saturation)** - Saturation, Hue, Alpha components 171 | - **[Utility Functions](https://react-beautiful-color.vercel.app/docs/utils)** - Color conversion utilities 172 | 173 | ## Support 174 | 175 | Buy Me A Coffee 176 | -------------------------------------------------------------------------------- /docs/components/ColorPickerExample.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Pipette } from 'lucide-react'; 4 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 5 | 6 | export function BasicColorPickerExample() { 7 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 8 | 9 | return ( 10 |
11 | 16 | 17 | 18 |
19 | 20 | 24 | 25 | 26 |
27 | 28 | 29 |
30 |
31 |
32 | 33 |
34 |
35 |

Color Values

36 |
37 |
38 | HEX 39 | {colorState.hex} 40 |
41 |
42 | RGB 43 | 44 | {colorState.rgb.r}, {colorState.rgb.g}, {colorState.rgb.b} 45 | 46 |
47 |
48 | HSL 49 | 50 | {colorState.hsl.h}°, {colorState.hsl.s}%, {colorState.hsl.l}% 51 | 52 |
53 |
54 | HSV 55 | 56 | {colorState.hsv.h}°, {colorState.hsv.s}%, {colorState.hsv.v}% 57 | 58 |
59 |
60 | Alpha 61 | {Math.round(colorState.alpha * 100)}% 62 |
63 |
64 |
65 | 66 |
67 |
\')', 74 | backgroundSize: '16px 16px', 75 | }), 76 | }} 77 | /> 78 |
79 |
80 |
81 | ); 82 | } 83 | 84 | export function HueExample() { 85 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 86 | 87 | return ( 88 |
89 |
90 |

Hue Selector

91 | 96 | 97 | 98 |
99 |
Current Hue: {Math.round(colorState.hsv.h)}°
100 |
101 | Color: {colorState.hex} 102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | 109 | export function SaturationExample() { 110 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 111 | 112 | return ( 113 |
114 |
115 |

Saturation Selector

116 | 121 | 122 | 123 |
124 |
125 |
Current Saturation: {Math.round(colorState.hsv.s)}%
126 |
Current Brightness: {Math.round(colorState.hsv.v)}%
127 |
128 | Color: {colorState.hex} 129 |
130 |
131 |
132 |
133 |
134 | ); 135 | } 136 | 137 | export function AlphaExample() { 138 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 139 | 140 | return ( 141 |
142 |
143 |

Alpha Selector

144 | 149 | 150 | 151 |
152 |
Current Alpha: {Math.round(colorState.alpha * 100)}%
153 |
154 |
155 |
156 | ); 157 | } 158 | 159 | export function EyeDropperExample() { 160 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 161 | 162 | return ( 163 |
164 |
165 |

Eye Dropper

166 | Selected Color: {colorState.hex} 167 |
168 |
169 | 174 | 175 | 179 | 180 | 181 | 182 |
183 |
184 |
188 |
189 |
190 |
191 |
192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /docs/content/docs/utils.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Color Utilities 3 | description: Public utility functions for color conversion and manipulation 4 | --- 5 | 6 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 7 | 8 | ## Overview 9 | 10 | React Beautiful Color provides a comprehensive set of utility functions for color conversion and manipulation. These utilities are available for public use and can help you work with colors in your applications. 11 | 12 | ## Color Conversion 13 | 14 | ### Hex Conversions 15 | 16 | 17 | 18 | ```tsx 19 | import { hexToRgb } from 'react-beautiful-color'; 20 | 21 | const rgb = hexToRgb('#ff6b9d'); 22 | // { r: 255, g: 107, b: 157 } 23 | ``` 24 | 25 | 26 | 27 | 28 | ```tsx 29 | import { hexToHsv } from 'react-beautiful-color'; 30 | 31 | const hsv = hexToHsv('#ff6b9d'); 32 | // { h: 334, s: 58, v: 100 } 33 | ``` 34 | 35 | 36 | 37 | 38 | ```tsx 39 | import { hexToHsl } from 'react-beautiful-color'; 40 | 41 | const hsl = hexToHsl('#ff6b9d'); 42 | // { h: 334, s: 100, l: 71 } 43 | ``` 44 | 45 | 46 | 47 | 48 | ### RGB Conversions 49 | 50 | 51 | 52 | ```tsx 53 | import { rgbToHex } from 'react-beautiful-color'; 54 | 55 | const hex = rgbToHex({ r: 255, g: 107, b: 157 }); 56 | // "#ff6b9d" 57 | ``` 58 | 59 | 60 | 61 | 62 | ```tsx 63 | import { rgbToHsv } from 'react-beautiful-color'; 64 | 65 | const hsv = rgbToHsv({ r: 255, g: 107, b: 157 }); 66 | // { h: 334, s: 58, v: 100 } 67 | ``` 68 | 69 | 70 | 71 | 72 | ```tsx 73 | import { rgbToHsl } from 'react-beautiful-color'; 74 | 75 | const hsl = rgbToHsl({ r: 255, g: 107, b: 157 }); 76 | // { h: 334, s: 100, l: 71 } 77 | ``` 78 | 79 | 80 | 81 | 82 | ### HSV Conversions 83 | 84 | 85 | 86 | ```tsx 87 | import { hsvToRgb } from 'react-beautiful-color'; 88 | 89 | const rgb = hsvToRgb({ h: 334, s: 58, v: 100 }); 90 | // { r: 255, g: 107, b: 157 } 91 | ``` 92 | 93 | 94 | 95 | 96 | ```tsx 97 | import { hsvToHex } from 'react-beautiful-color'; 98 | 99 | const hex = hsvToHex({ h: 334, s: 58, v: 100 }); 100 | // "#ff6b9d" 101 | ``` 102 | 103 | 104 | 105 | 106 | ```tsx 107 | import { hsvToHsl } from 'react-beautiful-color'; 108 | 109 | const hsl = hsvToHsl({ h: 334, s: 58, v: 100 }); 110 | // { h: 334, s: 100, l: 71 } 111 | ``` 112 | 113 | 114 | 115 | 116 | ### HSL Conversions 117 | 118 | 119 | 120 | ```tsx 121 | import { hslToRgb } from 'react-beautiful-color'; 122 | 123 | const rgb = hslToRgb({ h: 334, s: 100, l: 71 }); 124 | // { r: 255, g: 107, b: 157 } 125 | ``` 126 | 127 | 128 | 129 | 130 | ```tsx 131 | import { hslToHex } from 'react-beautiful-color'; 132 | 133 | const hex = hslToHex({ h: 334, s: 100, l: 71 }); 134 | // "#ff6b9d" 135 | ``` 136 | 137 | 138 | 139 | 140 | ```tsx 141 | import { hslToHsv } from 'react-beautiful-color'; 142 | 143 | const hsv = hslToHsv({ h: 334, s: 100, l: 71 }); 144 | // { h: 334, s: 58, v: 100 } 145 | ``` 146 | 147 | 148 | 149 | 150 | ## Utility Functions 151 | 152 | ### getContrastColor 153 | 154 | Returns the best contrast color (black or white) for accessibility. 155 | 156 | ```tsx 157 | import { getContrastColor } from 'react-beautiful-color'; 158 | 159 | const textColor = getContrastColor('#ff6b9d'); 160 | // "#000000" (black for light backgrounds) 161 | 162 | const textColor2 = getContrastColor('#1a1a1a'); 163 | // "#ffffff" (white for dark backgrounds) 164 | ``` 165 | 166 | ### randomHex 167 | 168 | Generates a random hex color. 169 | 170 | ```tsx 171 | import { randomHex } from 'react-beautiful-color'; 172 | 173 | const color = randomHex(); 174 | // "#a1b2c3" (random hex color) 175 | ``` 176 | 177 | ### formatColorString 178 | 179 | Formats a ColorState object into a CSS color string. 180 | 181 | ```tsx 182 | import { formatColorString, useColorState } from 'react-beautiful-color'; 183 | 184 | const [{ colorState }] = useColorState({ type: 'hex', value: '#ff6b9d' }); 185 | 186 | const hex = formatColorString(colorState, 'hex'); // "#ff6b9d" 187 | const rgb = formatColorString(colorState, 'rgb'); // "rgb(255, 107, 157)" 188 | const rgba = formatColorString(colorState, 'rgba'); // "rgba(255, 107, 157, 1)" 189 | const hsl = formatColorString(colorState, 'hsl'); // "hsl(334, 100%, 71%)" 190 | const hsla = formatColorString(colorState, 'hsla'); // "hsla(334, 100%, 71%, 1)" 191 | ``` 192 | 193 | ## Color Class 194 | 195 | The `Color` class provides an object-oriented interface for working with colors. It encapsulates color data and provides methods for conversion and formatting. 196 | 197 | ### Creating an Instance 198 | 199 | ```tsx 200 | import { Color } from 'react-beautiful-color'; 201 | 202 | // Create from hex 203 | const color = new Color({ type: 'hex', value: '#ff6b9d' }); 204 | 205 | // Create from RGB 206 | const color2 = new Color({ type: 'rgb', r: 255, g: 107, b: 157 }); 207 | 208 | // Create from HSL 209 | const color3 = new Color({ type: 'hsl', h: 334, s: 100, l: 71 }); 210 | 211 | // Create from HSV 212 | const color4 = new Color({ type: 'hsv', h: 334, s: 58, v: 100 }); 213 | ``` 214 | 215 | ### Color Conversion 216 | 217 | 218 | 219 | Returns the RGB representation of the color. 220 | 221 | ```tsx 222 | const color = new Color({ type: 'hex', value: '#ff6b9d' }); 223 | const rgb = color.getRgb(); 224 | // { r: 255, g: 107, b: 157 } 225 | ``` 226 | 227 | 228 | 229 | 230 | Returns the HSV representation of the color. 231 | 232 | ```tsx 233 | const hsv = color.getHsv(); 234 | // { h: 334, s: 58, v: 100 } 235 | ``` 236 | 237 | 238 | 239 | 240 | Returns the HSL representation of the color. 241 | 242 | ```tsx 243 | const hsl = color.getHsl(); 244 | // { h: 334, s: 100, l: 71 } 245 | ``` 246 | 247 | 248 | 249 | 250 | Returns the hex representation of the color. 251 | 252 | ```tsx 253 | const hex = color.getHex(); 254 | // "#ff6b9d" 255 | ``` 256 | 257 | 258 | 259 | 260 | Returns the RGBA representation of the color. 261 | 262 | ```tsx 263 | const rgba = color.getRgba(); 264 | // { r: 255, g: 107, b: 157, a: 1 } 265 | ``` 266 | 267 | 268 | 269 | 270 | Returns the HSLA representation of the color. 271 | 272 | ```tsx 273 | const hsla = color.getHsla(); 274 | // { h: 334, s: 100, l: 71, a: 1 } 275 | ``` 276 | 277 | 278 | 279 | 280 | Returns the HSVA representation of the color. 281 | 282 | ```tsx 283 | const hsva = color.getHsva(); 284 | // { h: 334, s: 58, v: 100, a: 1 } 285 | ``` 286 | 287 | 288 | 289 | 290 | ### Formatting Methods 291 | 292 | #### format() 293 | 294 | Formats the color as a CSS color string. Supports all formats except HSV/HSVA. 295 | 296 | ```tsx 297 | const color = new Color({ type: 'hex', value: '#ff6b9d' }); 298 | 299 | const hex = color.format('hex'); // "#ff6b9d" 300 | const rgb = color.format('rgb'); // "rgb(255, 107, 157)" 301 | const rgba = color.format('rgba'); // "rgba(255, 107, 157, 1)" 302 | const hsl = color.format('hsl'); // "hsl(334, 100%, 71%)" 303 | const hsla = color.format('hsla'); // "hsla(334, 100%, 71%, 1)" 304 | 305 | // Default format is hex 306 | const defaultFormat = color.format(); // "#ff6b9d" 307 | ``` 308 | 309 | #### getContrastingColor() 310 | 311 | Returns a new Color instance with the best contrast color (black or white) for accessibility. 312 | 313 | ```tsx 314 | const color = new Color({ type: 'hex', value: '#ff6b9d' }); 315 | const contrastColor = color.getContrastingColor(); 316 | const contrastHex = contrastColor.getHex(); // "#000000" (black for light backgrounds) 317 | 318 | const darkColor = new Color({ type: 'hex', value: '#1a1a1a' }); 319 | const darkContrast = darkColor.getContrastingColor(); 320 | const darkContrastHex = darkContrast.getHex(); // "#ffffff" (white for dark backgrounds) 321 | ``` 322 | 323 | ### Complete Example 324 | 325 | ```tsx 326 | import { Color } from 'react-beautiful-color'; 327 | 328 | // Create a color instance 329 | const color = new Color({ type: 'hex', value: '#ff6b9d' }); 330 | 331 | // Convert to different formats 332 | const rgb = color.getRgb(); // { r: 255, g: 107, b: 157 } 333 | const hsv = color.getHsv(); // { h: 334, s: 58, v: 100 } 334 | const hsl = color.getHsl(); // { h: 334, s: 100, l: 71 } 335 | 336 | // Format as CSS strings 337 | const hexString = color.format('hex'); // "#ff6b9d" 338 | const rgbString = color.format('rgb'); // "rgb(255, 107, 157)" 339 | const hslString = color.format('hsl'); // "hsl(334, 100%, 71%)" 340 | 341 | // Get contrasting color for accessibility 342 | const contrastColor = color.getContrastingColor(); 343 | const contrastHex = contrastColor.getHex(); // "#000000" 344 | ``` -------------------------------------------------------------------------------- /src/utils/public.ts: -------------------------------------------------------------------------------- 1 | import type { ColorFormat, ColorInput, HexColor, HslaColor, HslColor, HsvaColor, HsvColor, RgbaColor, RgbColor } from '../types'; 2 | import { Color } from '../types'; 3 | import { assertUnreachable } from './internal'; 4 | 5 | const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max); 6 | const round = (num: number): number => Math.round(clamp(num, 0, 255)); 7 | 8 | /** 9 | * Convert hex color to RGB values 10 | * @param hex - Hex color string (e.g., "#ff6b9d") 11 | * @returns RGB color object with r, g, b values (0-255) 12 | */ 13 | export const hexToRgb = (hex: string): RgbColor => { 14 | const h = hex.replace('#', ''); 15 | const bigint = parseInt(h, 16); 16 | return { 17 | r: (bigint >> 16) & 255, 18 | g: (bigint >> 8) & 255, 19 | b: bigint & 255, 20 | }; 21 | }; 22 | 23 | /** 24 | * Convert RGB values to hex color 25 | * @param rgb - RGB color object with r, g, b values (0-255) 26 | * @returns Hex color string (e.g., "#ff6b9d") 27 | */ 28 | export const rgbToHex = ({ r, g, b }: RgbColor): string => { 29 | const toHex = (c: number) => round(c).toString(16).padStart(2, '0'); 30 | return `#${toHex(r)}${toHex(g)}${toHex(b)}`; 31 | }; 32 | 33 | /** 34 | * Convert HSV color to RGB values 35 | * @param hsv - HSV color object with h (0-360), s (0-100), v (0-100) 36 | * @returns RGB color object with r, g, b values (0-255) 37 | */ 38 | export const hsvToRgb = ({ h, s, v }: HsvColor): RgbColor => { 39 | h = h / 360; 40 | s = s / 100; 41 | v = v / 100; 42 | 43 | const i = Math.floor(h * 6); 44 | const f = h * 6 - i; 45 | const p = v * (1 - s); 46 | const q = v * (1 - f * s); 47 | const t = v * (1 - (1 - f) * s); 48 | 49 | let r: number, g: number, b: number; 50 | 51 | switch (i % 6) { 52 | case 0: 53 | r = v; 54 | g = t; 55 | b = p; 56 | break; 57 | case 1: 58 | r = q; 59 | g = v; 60 | b = p; 61 | break; 62 | case 2: 63 | r = p; 64 | g = v; 65 | b = t; 66 | break; 67 | case 3: 68 | r = p; 69 | g = q; 70 | b = v; 71 | break; 72 | case 4: 73 | r = t; 74 | g = p; 75 | b = v; 76 | break; 77 | case 5: 78 | r = v; 79 | g = p; 80 | b = q; 81 | break; 82 | default: 83 | r = g = b = 0; 84 | } 85 | 86 | return { 87 | r: round(r * 255), 88 | g: round(g * 255), 89 | b: round(b * 255), 90 | }; 91 | }; 92 | 93 | /** 94 | * Convert RGB values to HSV color 95 | * @param rgb - RGB color object with r, g, b values (0-255) 96 | * @returns HSV color object with h (0-360), s (0-100), v (0-100) 97 | */ 98 | export const rgbToHsv = ({ r, g, b }: RgbColor): HsvColor => { 99 | r /= 255; 100 | g /= 255; 101 | b /= 255; 102 | 103 | const max = Math.max(r, g, b); 104 | const min = Math.min(r, g, b); 105 | const diff = max - min; 106 | 107 | let h = 0; 108 | const s = max === 0 ? 0 : diff / max; 109 | const v = max; 110 | 111 | if (diff !== 0) { 112 | switch (max) { 113 | case r: 114 | h = ((g - b) / diff + (g < b ? 6 : 0)) / 6; 115 | break; 116 | case g: 117 | h = ((b - r) / diff + 2) / 6; 118 | break; 119 | case b: 120 | h = ((r - g) / diff + 4) / 6; 121 | break; 122 | } 123 | } 124 | 125 | return { 126 | h: Math.round(h * 360), 127 | s: Math.round(s * 100), 128 | v: Math.round(v * 100), 129 | }; 130 | }; 131 | 132 | /** 133 | * Convert HSV color to hex string 134 | * @param hsv - HSV color object with h (0-360), s (0-100), v (0-100) 135 | * @returns Hex color string (e.g., "#ff6b9d") 136 | */ 137 | export const hsvToHex = (hsv: HsvColor): string => rgbToHex(hsvToRgb(hsv)); 138 | 139 | /** 140 | * Convert hex color to HSV values 141 | * @param hex - Hex color string (e.g., "#ff6b9d") 142 | * @returns HSV color object with h (0-360), s (0-100), v (0-100) 143 | */ 144 | export const hexToHsv = (hex: string): HsvColor => rgbToHsv(hexToRgb(hex)); 145 | 146 | /** 147 | * Convert RGB values to HSL color 148 | * @param rgb - RGB color object with r, g, b values (0-255) 149 | * @returns HSL color object with h (0-360), s (0-100), l (0-100) 150 | */ 151 | export const rgbToHsl = ({ r, g, b }: RgbColor): HslColor => { 152 | r /= 255; 153 | g /= 255; 154 | b /= 255; 155 | 156 | const max = Math.max(r, g, b); 157 | const min = Math.min(r, g, b); 158 | const diff = max - min; 159 | 160 | let h = 0; 161 | let s = 0; 162 | const l = (max + min) / 2; 163 | 164 | if (diff !== 0) { 165 | s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min); 166 | 167 | switch (max) { 168 | case r: 169 | h = ((g - b) / diff + (g < b ? 6 : 0)) / 6; 170 | break; 171 | case g: 172 | h = ((b - r) / diff + 2) / 6; 173 | break; 174 | case b: 175 | h = ((r - g) / diff + 4) / 6; 176 | break; 177 | } 178 | } 179 | 180 | return { 181 | h: Math.round(h * 360), 182 | s: Math.round(s * 100), 183 | l: Math.round(l * 100), 184 | }; 185 | }; 186 | 187 | /** 188 | * Convert HSL color to RGB values 189 | * @param hsl - HSL color object with h (0-360), s (0-100), l (0-100) 190 | * @returns RGB color object with r, g, b values (0-255) 191 | */ 192 | export const hslToRgb = ({ h, s, l }: HslColor): RgbColor => { 193 | h /= 360; 194 | s /= 100; 195 | l /= 100; 196 | 197 | const hue2rgb = (p: number, q: number, t: number): number => { 198 | if (t < 0) t += 1; 199 | if (t > 1) t -= 1; 200 | if (t < 1 / 6) return p + (q - p) * 6 * t; 201 | if (t < 1 / 2) return q; 202 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; 203 | return p; 204 | }; 205 | 206 | let r: number, g: number, b: number; 207 | 208 | if (s === 0) { 209 | r = g = b = l; 210 | } else { 211 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 212 | const p = 2 * l - q; 213 | r = hue2rgb(p, q, h + 1 / 3); 214 | g = hue2rgb(p, q, h); 215 | b = hue2rgb(p, q, h - 1 / 3); 216 | } 217 | 218 | return { 219 | r: Math.round(r * 255), 220 | g: Math.round(g * 255), 221 | b: Math.round(b * 255), 222 | }; 223 | }; 224 | 225 | /** 226 | * Convert HSL color to hex string 227 | * @param hsl - HSL color object with h (0-360), s (0-100), l (0-100) 228 | * @returns Hex color string (e.g., "#ff6b9d") 229 | */ 230 | export const hslToHex = (hsl: HslColor): string => rgbToHex(hslToRgb(hsl)); 231 | 232 | /** 233 | * Convert hex color to HSL values 234 | * @param hex - Hex color string (e.g., "#ff6b9d") 235 | * @returns HSL color object with h (0-360), s (0-100), l (0-100) 236 | */ 237 | export const hexToHsl = (hex: string): HslColor => rgbToHsl(hexToRgb(hex)); 238 | 239 | /** 240 | * Convert HSV color to HSL color 241 | * @param hsv - HSV color object with h (0-360), s (0-100), v (0-100) 242 | * @returns HSL color object with h (0-360), s (0-100), l (0-100) 243 | */ 244 | export const hsvToHsl = ({ h, s, v }: HsvColor): HslColor => { 245 | const l = (v * (2 - s / 100)) / 2; 246 | const sL = l !== 0 && l !== 100 ? ((v - l) / Math.min(l, 100 - l)) * 100 : 0; 247 | 248 | return { 249 | h, 250 | s: Math.round(sL), 251 | l: Math.round(l), 252 | }; 253 | }; 254 | 255 | /** 256 | * Convert HSL color to HSV color 257 | * @param hsl - HSL color object with h (0-360), s (0-100), l (0-100) 258 | * @returns HSV color object with h (0-360), s (0-100), v (0-100) 259 | */ 260 | export const hslToHsv = ({ h, s, l }: HslColor): HsvColor => { 261 | const v = l + (s * Math.min(l, 100 - l)) / 100; 262 | const sV = v === 0 ? 0 : 2 * (1 - l / v) * 100; 263 | 264 | return { 265 | h, 266 | s: Math.round(sV), 267 | v: Math.round(v), 268 | }; 269 | }; 270 | 271 | /** 272 | * Get the contrast color (black or white) for a given hex color 273 | * @param hex - Hex color string (e.g., "#ff6b9d") 274 | * @returns "#000000" for light colors, "#ffffff" for dark colors 275 | */ 276 | export const getContrastColor = (hex: string): string => { 277 | const { r, g, b } = hexToRgb(hex); 278 | const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 279 | return luminance > 0.5 ? '#000000' : '#ffffff'; 280 | }; 281 | 282 | /** 283 | * Generate a random hex color 284 | * @returns Random hex color string (e.g., "#a1b2c3") 285 | */ 286 | export const randomHex = (): string => { 287 | return `#${Math.floor(Math.random() * 16777215) 288 | .toString(16) 289 | .padStart(6, '0')}`; 290 | }; 291 | 292 | /** 293 | * Format a ColorState object into a CSS color string 294 | * @param color - ColorState object 295 | * @param format - Output format: 'hex', 'rgb', 'rgba', 'hsl', 'hsla' 296 | * @returns Formatted color string 297 | */ 298 | export const formatColorString = (color: Color, format: Exclude = 'hex'): string => { 299 | switch (format) { 300 | case 'hex': 301 | return color.getHex(); 302 | case 'rgb': 303 | const rgb = color.getRgb(); 304 | return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; 305 | case 'rgba': 306 | const rgba = color.getRgba(); 307 | return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`; 308 | case 'hsl': 309 | const hsl = color.getHsl(); 310 | return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; 311 | case 'hsla': 312 | const hsla = color.getHsla(); 313 | return `hsla(${hsla.h}, ${hsla.s}%, ${hsla.l}%, ${hsla.a})`; 314 | default: 315 | assertUnreachable(format); 316 | } 317 | }; 318 | 319 | function toHsva(color: ColorInput): HsvaColor { 320 | switch (color.type) { 321 | case 'hex': 322 | return { ...hexToHsv(color.value), a: 1 }; 323 | case 'rgb': 324 | return { ...rgbToHsv(color), a: 1 }; 325 | case 'rgba': 326 | return { ...rgbToHsv(color), a: color.a }; 327 | case 'hsl': 328 | return { ...hslToHsv(color), a: 1 }; 329 | case 'hsla': 330 | return { ...hslToHsv(color), a: 1 }; 331 | case 'hsv': 332 | return { ...color, a: 1 }; 333 | case 'hsva': 334 | return { ...color }; 335 | default: 336 | assertUnreachable(color); 337 | } 338 | } 339 | 340 | export function convertColor(color: ColorInput, format: 'hex'): HexColor; 341 | export function convertColor(color: ColorInput, format: 'rgb'): RgbColor; 342 | export function convertColor(color: ColorInput, format: 'rgba'): RgbaColor; 343 | export function convertColor(color: ColorInput, format: 'hsl'): HslColor; 344 | export function convertColor(color: ColorInput, format: 'hsla'): HslaColor; 345 | export function convertColor(color: ColorInput, format: 'hsv'): HsvColor; 346 | export function convertColor(color: ColorInput, format: 'hsva'): HsvaColor; 347 | export function convertColor(color: ColorInput, format: ColorFormat): HexColor | RgbColor | RgbaColor | HslColor | HslaColor | HsvColor | HsvaColor { 348 | const hsva = toHsva(color); 349 | switch (format) { 350 | case 'hex': 351 | return hsvToHex(hsva); 352 | case 'rgb': 353 | return hsvToRgb(hsva); 354 | case 'rgba': 355 | return { ...hsvToRgb(hsva), a: hsva.a }; 356 | case 'hsl': 357 | return hsvToHsl(hsva); 358 | case 'hsla': 359 | return { ...hsvToHsl(hsva), a: hsva.a }; 360 | case 'hsv': 361 | return { h: hsva.h, s: hsva.s, v: hsva.v }; 362 | case 'hsva': 363 | return hsva; 364 | default: 365 | assertUnreachable(format); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /docs/content/docs/components/color-picker.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: ColorPicker 3 | description: A complete color picker component with hue, saturation, and alpha controls 4 | --- 5 | 6 | import { BasicColorPickerExample 7 | } from '../../../components/ColorPickerExample'; 8 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 9 | 10 | ## Features 11 | 12 | - **Compound Components**: Modular design with sub-components 13 | - **Eye Dropper**: Built-in screen color picker (where supported) 14 | - **Touch & Mouse Support**: Works on all devices 15 | - **Color Formats**: Supports hex, rgb, rgba, hsl, hsla, hsv, hsva 16 | 17 | ## Basic Usage 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ```tsx 26 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 27 | import { Pipette } from 'lucide-react'; 28 | 29 | export function BasicColorPickerExample() { 30 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hex', value: '#ff6b9d' }); 31 | 32 | return ( 33 |
34 | 39 | 40 |
41 | 42 | 46 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | 54 |
55 |
56 |

Color Values

57 |
58 |
59 | HEX 60 | {colorState.hex} 61 |
62 |
63 | RGB 64 | 65 | {colorState.rgb.r}, {colorState.rgb.g}, {colorState.rgb.b} 66 | 67 |
68 |
69 | HSL 70 | 71 | {colorState.hsl.h}°, {colorState.hsl.s}%, {colorState.hsl.l}% 72 | 73 |
74 |
75 | HSV 76 | 77 | {colorState.hsv.h}°, {colorState.hsv.s}%, {colorState.hsv.v}% 78 | 79 |
80 |
81 | Alpha 82 | {Math.round(colorState.alpha * 100)}% 83 |
84 |
85 |
86 | 87 |
88 |
\')', 95 | backgroundSize: '16px 16px', 96 | }), 97 | }} 98 | /> 99 |
100 |
101 |
102 | ); 103 | } 104 | ``` 105 | 106 | 107 | 108 | ```tsx 109 | import { ColorPicker, Color } from 'react-beautiful-color'; 110 | import { Pipette } from 'lucide-react'; 111 | 112 | export function BasicColorPickerExampleUseState() { 113 | const [color, setColor] = useState(new Color({ type: 'hex', value: '#ff6b9d' })); 114 | 115 | const rgba = color.getRgba(); 116 | const hex = color.getHex(); 117 | const hsl = color.getHsl(); 118 | const hsv = color.getHsv(); 119 | 120 | return ( 121 |
122 | 127 | 128 | 129 |
130 | 131 | 135 | 136 | 137 |
138 | 139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 |

Color Values

147 |
148 |
149 | HEX 150 | {hex} 151 |
152 |
153 | RGB 154 | 155 | {rgba.r}, {rgba.g}, {rgba.b} 156 | 157 |
158 |
159 | HSL 160 | 161 | {hsl.h}°, {hsl.s}%, {hsl.l}% 162 | 163 |
164 |
165 | HSV 166 | 167 | {hsv.h}°, {hsv.s}%, {hsv.v}% 168 | 169 |
170 |
171 | Alpha 172 | {Math.round(rgba.a * 100)}% 173 |
174 |
175 |
176 | 177 |
178 |
\')', 185 | backgroundSize: '16px 16px', 186 | }), 187 | }} 188 | /> 189 |
190 |
191 |
192 | ); 193 | } 194 | ``` 195 | 196 | 197 | 198 | ## Props 199 | 200 | | Prop | Type | Default | Description | 201 | |------|------|---------|-------------| 202 | | `defaultColor` | [ColorInput](/docs/components/color-picker#color-input-format) \| [Color](/docs/utils#color-class) | - | Initial color value | 203 | | `color` | [ColorInput](/docs/components/color-picker#color-input-format) \| [Color](/docs/utils#color-class) | - | Color value | 204 | | `onChange` | `(color: Color) => void` | - | Callback fired when color changes | 205 | | `className` | `string` | - | Additional CSS classes | 206 | 207 | ## Color Input Format 208 | 209 | The `ColorInput` type supports multiple color formats: 210 | 211 | 212 | 213 | ```tsx 214 | // Hex color input 215 | const hexColor = { type: 'hex', value: '#ff6b9d' }; 216 | ``` 217 | 218 | 219 | 220 | ```tsx 221 | // RGB color input 222 | const rgbColor = { type: 'rgb', r: 255, g: 107, b: 157 }; 223 | 224 | // RGBA color input (with alpha) 225 | const rgbaColor = { type: 'rgba', r: 255, g: 107, b: 157, a: 0.8 }; 226 | ``` 227 | 228 | 229 | 230 | ```tsx 231 | // HSL color input 232 | const hslColor = { type: 'hsl', h: 334, s: 100, l: 71 }; 233 | 234 | // HSLA color input (with alpha) 235 | const hslaColor = { type: 'hsla', h: 334, s: 100, l: 71, a: 0.8 }; 236 | ``` 237 | 238 | 239 | 240 | ```tsx 241 | // HSV color input 242 | const hsvColor = { type: 'hsv', h: 334, s: 58, v: 100 }; 243 | 244 | // HSVA color input (with alpha) 245 | const hsvaColor = { type: 'hsva', h: 334, s: 58, v: 100, a: 0.8 }; 246 | ``` 247 | 248 | 249 | 250 | ## Sub-Components 251 | 252 | ### ColorPicker.Saturation 253 | 254 | The saturation and brightness selection area. 255 | 256 | ```tsx 257 | 258 | ``` 259 | 260 | ### ColorPicker.Hue 261 | 262 | The hue selection slider. 263 | 264 | ```tsx 265 | 266 | ``` 267 | 268 | ### ColorPicker.Alpha 269 | 270 | The alpha/transparency selection slider. 271 | 272 | ```tsx 273 | 274 | ``` 275 | 276 | ### ColorPicker.EyeDropper 277 | 278 | Button to activate the browser's eye dropper tool. The button is not rendered if the user's browser doesn't support it. 279 | 280 | ```tsx 281 | 282 | {/* Icon content */} 283 | 284 | ``` 285 | -------------------------------------------------------------------------------- /docs/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { Palette, Zap, Code, Sparkles, Pipette, Github, Package, Coffee, Heart } from 'lucide-react'; 5 | import { ColorPicker, useColorState } from 'react-beautiful-color'; 6 | import { cn } from 'fumadocs-ui/utils/cn'; 7 | 8 | export default function HomePage() { 9 | const [{ colorInput, colorState }, setColor] = useColorState({ type: 'hsva', h: 334, s: 100, v: 100, a: 0.5 }); 10 | 11 | return ( 12 |
13 | {/* Hero Section */} 14 |
15 |
16 | {/* Badge */} 17 |
18 | 19 | Beautiful • Flexible • Type-Safe 20 |
21 | 22 | {/* Hero Title */} 23 |
24 |

25 | react- 26 | 30 | beautiful 31 | 32 | -color 33 |

34 |
35 |

The most flexible and beautiful color picker for React

36 |
37 | 38 |
39 | {/* Social Links */} 40 |
41 | 47 | 51 | GitHub 52 | 53 | 59 | 63 | npm 64 | 65 | 69 | 73 | Docs 74 | 75 |
76 |
77 |
78 | 79 | {/* Hero Subtitle */} 80 | 81 |
82 | 87 | 88 | 89 |
90 | 91 | 95 | 96 | 97 |
98 | 99 | 100 |
101 |
102 |
103 |
104 | 105 | {/* Made with love */} 106 |
107 |
108 | Made with 109 | 113 | for 114 |
115 | 122 | 126 | 127 | React 128 |
129 | by 130 | 134 | Özer Gökalpsezer 135 | 136 |
137 | 143 | 147 | Buy me a coffee 148 | 149 |
150 |
151 |
152 | 153 | {/* Features Section */} 154 |
155 |
156 |
157 |

Why Choose React Beautiful Color?

158 |

159 | Built for developers who care about flexibility, performance, and beautiful user experiences. 160 |

161 |
162 | 163 |
164 | {/* Feature 1 */} 165 |
166 |
167 | 168 |
169 |

Compound Components

170 |

Compose your own layout with flexible compound components. No rigid UI constraints.

171 |
172 | 173 | {/* Feature 2 */} 174 |
175 |
176 | 177 |
178 |

Beautiful Design

179 |

Clean, modern UI that fits any design system. Built with Tailwind CSS for easy customization.

180 |
181 | 182 | {/* Feature 3 */} 183 |
184 |
185 | 186 |
187 |

Powerful Hook

188 |

useColorState hook with all color formats instantly available and complete type safety.

189 |
190 | 191 | {/* Feature 4 */} 192 |
193 |
194 | 195 |
196 |

Lightweight

197 |

Pure Tailwind CSS with no external dependencies. Small bundle size, maximum performance.

198 |
199 | 200 | {/* Feature 5 */} 201 |
202 |
203 | 204 |
205 |

Type-Safe

206 |

Full TypeScript support with discriminated unions for color inputs and complete type safety.

207 |
208 | 209 | {/* Feature 6 */} 210 |
211 |
212 | 213 |
214 |

Eye Dropper

215 |

Built-in eye dropper support for picking colors from anywhere on the screen.

216 |
217 |
218 |
219 |
220 |
221 | ); 222 | } 223 | --------------------------------------------------------------------------------