diff --git a/docs/app/components/content/examples/color-picker/ColorPickerChooserExample.vue b/docs/app/components/content/examples/color-picker/ColorPickerChooserExample.vue
new file mode 100644
index 0000000000..61b0f8bcb3
--- /dev/null
+++ b/docs/app/components/content/examples/color-picker/ColorPickerChooserExample.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/content.config.ts b/docs/content.config.ts
index 74c7b33ce0..604bb580d1 100644
--- a/docs/content.config.ts
+++ b/docs/content.config.ts
@@ -39,7 +39,7 @@ export const collections = {
type: 'page',
source: [{
include: '**/*'
- }, pro!],
+ }, pro!].filter(Boolean),
schema
})
}
diff --git a/docs/content/3.components/color-picker.md b/docs/content/3.components/color-picker.md
new file mode 100644
index 0000000000..8495c96c4e
--- /dev/null
+++ b/docs/content/3.components/color-picker.md
@@ -0,0 +1,150 @@
+---
+title: ColorPicker
+description: A component to select a color.
+links:
+ - label: GitHub
+ icon: i-simple-icons-github
+ to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/ColorPicker.vue
+navigation.badge: New
+---
+
+## Usage
+
+Use the `v-model` directive to control the value of the ColorPicker.
+
+::component-code
+---
+ignore:
+ - modelValue
+external:
+ - modelValue
+props:
+ modelValue: '#00C16A'
+---
+::
+
+Use the `default-value` prop to set the initial value when you do not need to control its state.
+
+::component-code
+---
+ignore:
+ - defaultValue
+props:
+ defaultValue: '#00BCD4'
+---
+::
+
+### RGB Format
+
+Use the `format` prop to set `rgb` value of the ColorPicker.
+
+::component-code
+---
+ignore:
+ - modelValue
+ - format
+external:
+ - modelValue
+props:
+ format: rgb
+ modelValue: 'rgb(0, 193, 106)'
+---
+::
+
+### HSL Format
+
+Use the `format` prop to set `hsl` value of the ColorPicker.
+
+::component-code
+---
+ignore:
+ - modelValue
+ - format
+external:
+ - modelValue
+props:
+ format: hsl
+ modelValue: 'hsl(153, 100%, 37.8%)'
+---
+::
+
+### HWB Format
+
+Use the `format` prop to set `hwb` value of the ColorPicker.
+
+::component-code
+---
+ignore:
+ - modelValue
+ - format
+external:
+ - modelValue
+props:
+ format: hwb
+ modelValue: 'hwb(150, 0%, 24%)'
+---
+::
+
+### Throttle
+
+Use the `throttle` prop to set the throttle value of the ColorPicker.
+
+::component-code
+---
+ignore:
+ - modelValue
+external:
+ - modelValue
+props:
+ throttle: 100
+ modelValue: '#00C16A'
+---
+::
+
+### Size
+
+Use the `size` prop to set the size of the ColorPicker.
+
+::component-code
+---
+props:
+ size: xl
+---
+::
+
+### Disabled
+
+Use the `disabled` prop to disable the ColorPicker.
+
+::component-code
+---
+props:
+ disabled: true
+---
+::
+
+## Examples
+
+### As a Color chooser
+
+Use a [Button](/components/button) and a [Popover](/components/popover) component to create a color chooser.
+
+::component-example
+---
+name: 'color-picker-chooser-example'
+---
+::
+
+## API
+
+### Props
+
+:component-props
+
+### Emits
+
+:component-emits
+
+## Theme
+
+:component-theme
diff --git a/package.json b/package.json
index 3b716c1de1..1ac48e6f4e 100644
--- a/package.json
+++ b/package.json
@@ -85,9 +85,11 @@
"@tailwindcss/postcss": "4.0.0-beta.5",
"@tailwindcss/vite": "4.0.0-beta.5",
"@tanstack/vue-table": "^8.20.5",
+ "@types/color": "^4.2.0",
"@unhead/vue": "^1.11.13",
"@vueuse/core": "^12.0.0",
"@vueuse/integrations": "^12.0.0",
+ "color": "^4.2.3",
"consola": "^3.2.3",
"defu": "^6.1.4",
"embla-carousel-auto-height": "^8.5.1",
diff --git a/playground/app/app.vue b/playground/app/app.vue
index cd2ff605d9..1f50516bad 100644
--- a/playground/app/app.vue
+++ b/playground/app/app.vue
@@ -29,6 +29,7 @@ const components = [
'checkbox',
'chip',
'collapsible',
+ 'color-picker',
'context-menu',
'command-palette',
'drawer',
diff --git a/playground/app/pages/components/color-picker.vue b/playground/app/pages/components/color-picker.vue
new file mode 100644
index 0000000000..b6030162c3
--- /dev/null
+++ b/playground/app/pages/components/color-picker.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ {{ colorHex }}
+
+
+
+
+ Purple
+
+
+ Lime
+
+
+ Tomato
+
+
+
+
console.log('model update')" />
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cd663c3247..6ab35657d2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -51,6 +51,9 @@ importers:
'@tanstack/vue-table':
specifier: ^8.20.5
version: 8.20.5(vue@3.5.13(typescript@5.6.3))
+ '@types/color':
+ specifier: ^4.2.0
+ version: 4.2.0
'@unhead/vue':
specifier: ^1.11.13
version: 1.11.13(vue@3.5.13(typescript@5.6.3))
@@ -60,6 +63,9 @@ importers:
'@vueuse/integrations':
specifier: ^12.0.0
version: 12.0.0(change-case@5.4.4)(fuse.js@7.0.0)(typescript@5.6.3)
+ color:
+ specifier: ^4.2.3
+ version: 4.2.3
consola:
specifier: ^3.2.3
version: 3.2.3
@@ -2230,6 +2236,15 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
+ '@types/color-convert@2.0.4':
+ resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==}
+
+ '@types/color-name@1.1.5':
+ resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==}
+
+ '@types/color@4.2.0':
+ resolution: {integrity: sha512-6+xrIRImMtGAL2X3qYkd02Mgs+gFGs+WsK0b7VVMaO4mYRISwyTjcqNrO0mNSmYEoq++rSLDB2F5HDNmqfOe+A==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -9464,6 +9479,16 @@ snapshots:
'@trysound/sax@0.2.0': {}
+ '@types/color-convert@2.0.4':
+ dependencies:
+ '@types/color-name': 1.1.5
+
+ '@types/color-name@1.1.5': {}
+
+ '@types/color@4.2.0':
+ dependencies:
+ '@types/color-convert': 2.0.4
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 0.7.34
@@ -10498,7 +10523,6 @@ snapshots:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
- optional: true
color-support@1.1.3: {}
@@ -10506,7 +10530,6 @@ snapshots:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
- optional: true
colord@2.9.3: {}
@@ -12129,8 +12152,7 @@ snapshots:
is-arrayish@0.2.1: {}
- is-arrayish@0.3.2:
- optional: true
+ is-arrayish@0.3.2: {}
is-binary-path@2.1.0:
dependencies:
@@ -14736,7 +14758,6 @@ snapshots:
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
- optional: true
sirv@2.0.4:
dependencies:
diff --git a/src/runtime/components/ColorPicker.vue b/src/runtime/components/ColorPicker.vue
new file mode 100644
index 0000000000..a253b9f9b5
--- /dev/null
+++ b/src/runtime/components/ColorPicker.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts
index e89e082426..e407e79d52 100644
--- a/src/runtime/types/index.ts
+++ b/src/runtime/types/index.ts
@@ -12,6 +12,7 @@ export * from '../components/Carousel.vue'
export * from '../components/Checkbox.vue'
export * from '../components/Chip.vue'
export * from '../components/Collapsible.vue'
+export * from '../components/ColorPicker.vue'
export * from '../components/CommandPalette.vue'
export * from '../components/Container.vue'
export * from '../components/ContextMenu.vue'
diff --git a/src/theme/color-picker.ts b/src/theme/color-picker.ts
new file mode 100644
index 0000000000..00e1638220
--- /dev/null
+++ b/src/theme/color-picker.ts
@@ -0,0 +1,39 @@
+export default {
+ slots: {
+ root: 'data-[disabled]:opacity-75',
+ picker: 'flex gap-4',
+ selector: 'rounded-[calc(var(--ui-radius)*1.5)]',
+ selectorBackground: 'w-full h-full relative rounded-[calc(var(--ui-radius)*1.2)]',
+ selectorThumb: '-translate-y-1/2 -translate-x-1/2 absolute size-4 ring-2 ring-[var(--color-white)] rounded-full cursor-pointer data-[disabled]:cursor-not-allowed',
+ track: 'w-[8px] relative rounded-[calc(var(--ui-radius)*1.5)]',
+ trackThumb: 'absolute transform -translate-y-1/2 -translate-x-[4px] size-4 rounded-full ring-2 ring-[var(--color-white)] cursor-pointer data-[disabled]:cursor-not-allowed'
+ },
+ variants: {
+ size: {
+ xs: {
+ selector: 'w-38 h-38',
+ track: 'h-38'
+ },
+ sm: {
+ selector: 'w-40 h-40',
+ track: 'h-40'
+ },
+ md: {
+ selector: 'w-42 h-42',
+ track: 'h-42'
+ },
+ lg: {
+ selector: 'w-44 h-44',
+ track: 'h-44'
+ },
+ xl: {
+ selector: 'w-46 h-46',
+ track: 'h-46'
+ }
+ }
+ },
+ compoundVariants: [],
+ defaultVariants: {
+ size: 'md'
+ }
+}
diff --git a/src/theme/index.ts b/src/theme/index.ts
index c5af513a97..f4c7f753ff 100644
--- a/src/theme/index.ts
+++ b/src/theme/index.ts
@@ -12,6 +12,7 @@ export { default as carousel } from './carousel'
export { default as checkbox } from './checkbox'
export { default as chip } from './chip'
export { default as collapsible } from './collapsible'
+export { default as colorPicker } from './color-picker'
export { default as commandPalette } from './command-palette'
export { default as container } from './container'
export { default as contextMenu } from './context-menu'
diff --git a/test/components/ColorPicker.spec.ts b/test/components/ColorPicker.spec.ts
new file mode 100644
index 0000000000..8c706c248b
--- /dev/null
+++ b/test/components/ColorPicker.spec.ts
@@ -0,0 +1,37 @@
+import { describe, it, expect, test } from 'vitest'
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+import ColorPicker, { type ColorPickerProps } from '../../src/runtime/components/ColorPicker.vue'
+import ComponentRender from '../component-render'
+import theme from '#build/ui/color-picker'
+
+describe('ColorPicker', () => {
+ const sizes = Object.keys(theme.variants.size) as any
+ const formats = [
+ ['hex', '#00C16A'],
+ ['rgb', 'rgb(0, 193, 106)'],
+ ['hsl', 'hsl(153, 100%, 37.8%)'],
+ ['hwb', 'hwb(150, 0%, 24%)']
+ ]
+
+ it.each([
+ // Props
+ ['with disabled', { props: { disabled: true } }],
+ ...sizes.map((size: string) => [`with size ${size}`, { props: { size } }]),
+ ...formats.map(format => [`with format ${format[0]}`, { props: { format: format[0], defaultValue: format[1] } }]),
+ ['with as', { props: { as: 'section' } }],
+ ['with class', { props: { class: 'w-96' } }],
+ ['with ui', { props: { ui: { picker: 'gap-8' } } }]
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ColorPickerProps }) => {
+ const html = await ComponentRender(nameOrHtml, options, ColorPicker)
+ expect(html).toMatchSnapshot()
+ })
+
+ describe('emits', () => {
+ test('update:modelValue event', async () => {
+ const wrapper = await mountSuspended(ColorPicker)
+ await wrapper.setValue('#00C16A')
+
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [['#00C16A']] })
+ })
+ })
+})
diff --git a/test/components/__snapshots__/ColorPicker-vue.spec.ts.snap b/test/components/__snapshots__/ColorPicker-vue.spec.ts.snap
new file mode 100644
index 0000000000..d457a02aab
--- /dev/null
+++ b/test/components/__snapshots__/ColorPicker-vue.spec.ts.snap
@@ -0,0 +1,196 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`ColorPicker > renders with as correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with class correctly 1`] = `
+"
"
+`;
+
+exports[`ColorPicker > renders with disabled correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format hex correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format hsl correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format hwb correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format rgb correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size lg correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size md correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size sm correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size xl correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size xs correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with ui correctly 1`] = `
+""
+`;
diff --git a/test/components/__snapshots__/ColorPicker.spec.ts.snap b/test/components/__snapshots__/ColorPicker.spec.ts.snap
new file mode 100644
index 0000000000..ac012573de
--- /dev/null
+++ b/test/components/__snapshots__/ColorPicker.spec.ts.snap
@@ -0,0 +1,196 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`ColorPicker > renders with as correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with class correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with disabled correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format hex correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format hsl correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format hwb correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with format rgb correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size lg correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size md correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size sm correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size xl correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with size xs correctly 1`] = `
+""
+`;
+
+exports[`ColorPicker > renders with ui correctly 1`] = `
+""
+`;