diff --git a/apps/www/src/app/examples/color-picker/page.tsx b/apps/www/src/app/examples/color-picker/page.tsx
new file mode 100644
index 000000000..73a46e751
--- /dev/null
+++ b/apps/www/src/app/examples/color-picker/page.tsx
@@ -0,0 +1,170 @@
+'use client';
+
+import { Button, ColorPicker, Flex, Popover, Text } from '@raystack/apsara';
+import { useState } from 'react';
+
+const cardStyle = {
+ width: 280,
+ padding: 16,
+ borderRadius: 8,
+ background: 'var(--rs-color-background-base-primary)',
+ border: '1px solid var(--rs-color-border-base-primary)'
+} as const;
+
+export default function ColorPickerExamplesPage() {
+ const [controlledValue, setControlledValue] = useState(
+ 'oklch(0.5438 0.191 267.01)'
+ );
+ const [controlledMode, setControlledMode] = useState<
+ 'hex' | 'rgb' | 'hsl' | 'oklch'
+ >('oklch');
+ const [popoverColor, setPopoverColor] = useState('#DA2929');
+
+ return (
+
+
+
+ ColorPicker
+
+
+ The picker edits internally in OKLCH for perceptual uniformity. The
+ area pad shows a chroma × lightness cross-section at the selected hue;
+ the mode prop selects the output format (hex / rgb / hsl
+ / oklch).
+
+
+
+
+
+
+ Default (hex)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ OKLCH mode
+
+
+ defaultValue='oklch(0.5438 0.191 267.01)'{' '}
+ with defaultMode='oklch'.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Controlled — emits live value
+
+ {
+ setControlledValue(value);
+ setControlledMode(mode as typeof controlledMode);
+ }}
+ onModeChange={mode =>
+ setControlledMode(mode as typeof controlledMode)
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ value:
+
+
+ {controlledValue}
+
+
+ mode: {controlledMode}
+
+
+
+
+
+
+ Popover trigger
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {popoverColor}
+
+
+
+
+ );
+}
diff --git a/apps/www/src/content/docs/components/color-picker/demo.ts b/apps/www/src/content/docs/components/color-picker/demo.ts
index 37e72e9bd..085855bde 100644
--- a/apps/www/src/content/docs/components/color-picker/demo.ts
+++ b/apps/www/src/content/docs/components/color-picker/demo.ts
@@ -16,7 +16,7 @@ style={{
-
+
`
};
@@ -36,6 +36,45 @@ export const basicDemo = {
`
};
+export const copyableDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+
+
+`
+};
+
+export const oklchDemo = {
+ type: 'code',
+ code: `
+
+
+
+
+
+
+
+`
+};
+
export const popoverDemo = {
type: 'code',
previewCode: false,
@@ -68,7 +107,7 @@ export const popoverDemo = {
-
+
diff --git a/apps/www/src/content/docs/components/color-picker/index.mdx b/apps/www/src/content/docs/components/color-picker/index.mdx
index 9722535ce..793b63dd3 100644
--- a/apps/www/src/content/docs/components/color-picker/index.mdx
+++ b/apps/www/src/content/docs/components/color-picker/index.mdx
@@ -7,6 +7,8 @@ source: packages/raystack/components/color-picker
import {
preview,
basicDemo,
+ copyableDemo,
+ oklchDemo,
popoverDemo,
} from "./demo.ts";
@@ -24,7 +26,7 @@ import { ColorPicker } from '@raystack/apsara'
-
+
```
@@ -38,7 +40,7 @@ The `ColorPicker` is composed of several subcomponents, each responsible for a s
### Area
-Enables users to select a color from a palette. Typically used for choosing saturation and brightness.
+Enables users to select a color from a 2D palette. The surface adapts to the active mode: in `hex`, `rgb`, and `hsl` modes it renders the classic saturation × brightness gradient square; in `oklch` mode it renders a chroma × lightness plane covering the full P3 gamut.
### Hue
@@ -50,13 +52,13 @@ Provides a slider for selecting the alpha value of the color.
### Mode
-Lets users switch between different color models (e.g., HEX, RGB, HSL) via a dropdown menu.
+Lets users switch between different color models (HEX, RGB, HSL, OKLCH) via a dropdown menu.
### Input
-Displays the current color value in the selected color model and allows direct text input.
+Displays the current color value in the selected color model as a read-only string. The value is updated automatically as the user interacts with the area, hue, or alpha controls. Pass `copyable` to render a copy-to-clipboard button in the input's trailing slot.
## Examples
@@ -64,6 +66,18 @@ Displays the current color value in the selected color model and allows direct t
+### Copy to Clipboard
+
+Pass the `copyable` prop on `ColorPicker.Input` to render a copy-to-clipboard button in the input's trailing slot. The button copies the formatted color string in the active mode (e.g. `#FF0000`, `rgb(...)`, or `oklch(...)`) and shows a brief confirmation icon after a successful copy.
+
+
+
+### OKLCH Mode
+
+The picker stores color internally in OKLCH. In `hex`, `rgb`, and `hsl` modes the area pad is the familiar HSL saturation × brightness square and the hue slider runs in HSL hue — so the picker behaves like a traditional color picker and only emits colors representable in the chosen format. Pass an `oklch(...)` string as `defaultValue` and set `defaultMode='oklch'` to switch to a chroma × lightness plane covering the full P3 gamut; the hue slider then runs in OKLCH hue, where equal steps correspond to equal perceptual changes.
+
+
+
### Popover Integration
The `ColorPicker` can be embedded within a `Popover` component to create a more interactive and space-efficient color selection experience.
diff --git a/apps/www/src/content/docs/components/color-picker/props.ts b/apps/www/src/content/docs/components/color-picker/props.ts
index 7a399ebde..f67fdcc4b 100644
--- a/apps/www/src/content/docs/components/color-picker/props.ts
+++ b/apps/www/src/content/docs/components/color-picker/props.ts
@@ -3,7 +3,7 @@
*/
export interface ColorPickerProps {
/**
- * The controlled color value. Accepts hex, rgb, hsl, etc.
+ * The controlled color value. Accepts hex, rgb, hsl, oklch, etc.
*/
value?: string;
/**
@@ -16,14 +16,14 @@ export interface ColorPickerProps {
*/
onValueChange?: (value: string, mode: string) => void;
/**
- * The initial color mode (hex, rgb, hsl).
+ * The initial color mode (hex, rgb, hsl, oklch).
* @default 'hex'
*/
- defaultMode?: 'hex' | 'rgb' | 'hsl';
+ defaultMode?: 'hex' | 'rgb' | 'hsl' | 'oklch';
/**
* The controlled color mode.
*/
- mode?: 'hex' | 'rgb' | 'hsl';
+ mode?: 'hex' | 'rgb' | 'hsl' | 'oklch';
/**
* Callback fired when the color mode changes.
*/
@@ -36,7 +36,7 @@ export interface ColorPickerProps {
export interface ColorPickerModeProps {
/**
* Supported color modes for the picker.
- * @default ['hex', 'rgb', 'hsl']
+ * @default ['hex', 'rgb', 'hsl', 'oklch']
*/
- options?: Array<'hex' | 'rgb' | 'hsl'>;
+ options?: Array<'hex' | 'rgb' | 'hsl' | 'oklch'>;
}
diff --git a/packages/raystack/components/color-picker/__tests__/color-picker.test.tsx b/packages/raystack/components/color-picker/__tests__/color-picker.test.tsx
index 5d685f2a5..033818d89 100644
--- a/packages/raystack/components/color-picker/__tests__/color-picker.test.tsx
+++ b/packages/raystack/components/color-picker/__tests__/color-picker.test.tsx
@@ -1,7 +1,12 @@
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ColorPicker } from '../color-picker';
+const mockCopy = vi.fn();
+vi.mock('~/hooks/useCopyToClipboard', () => ({
+ useCopyToClipboard: () => ({ copy: mockCopy })
+}));
+
// // Mock ResizeObserver for tests
// const originalResizeObserver = global.ResizeObserver;
// beforeAll(() => {
@@ -66,17 +71,30 @@ describe('ColorPicker', () => {
expect(area).toHaveClass('custom-area');
});
- it('renders with background gradient', () => {
+ it('renders the gradient canvas in oklch mode', () => {
render(
-
+
);
const area = screen.getByTestId('color-area');
- expect(area).toHaveStyle(
- 'background: linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)), linear-gradient(90deg, rgba(255,255,255,1), rgba(255,255,255,0)), hsl(0, 100%, 50%)'
+ expect(area.querySelector('canvas')).toBeInTheDocument();
+ });
+
+ it('renders the HSL gradient surface in non-oklch modes', () => {
+ render(
+
+
+
);
+
+ const area = screen.getByTestId('color-area');
+ // Non-oklch modes use a CSS-gradient div, not the canvas-painted
+ // OKLCH plane — the absence of