diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 6a01653e49..328ed332e5 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -157,6 +157,16 @@ export default defineConfig({
{ text: 'Toggle Group', link: '/docs/components/toggle-group' },
],
},
+ {
+ text: 'Color',
+ items: [
+ { text: 'Color Swatch', link: '/docs/components/color-swatch' },
+ {
+ text: 'Color Swatch Picker',
+ link: '/docs/components/color-swatch-picker',
+ },
+ ],
+ },
{
text: 'Dates',
items: [
diff --git a/docs/components/demo/ColorSwatch/css/index.vue b/docs/components/demo/ColorSwatch/css/index.vue
new file mode 100644
index 0000000000..b0413f57b7
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/css/index.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatch/css/styles.css b/docs/components/demo/ColorSwatch/css/styles.css
new file mode 100644
index 0000000000..1cf37ca441
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/css/styles.css
@@ -0,0 +1,11 @@
+.swatch {
+ width: 32px;
+ height: 32px;
+ border-radius: 4px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
+ flex-shrink: 0;
+}
+
+.dark .swatch {
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
+}
\ No newline at end of file
diff --git a/docs/components/demo/ColorSwatch/tailwind/index.vue b/docs/components/demo/ColorSwatch/tailwind/index.vue
new file mode 100644
index 0000000000..89f9048993
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/tailwind/index.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatch/tailwind/tailwind.config.js b/docs/components/demo/ColorSwatch/tailwind/tailwind.config.js
new file mode 100644
index 0000000000..fe691bdd5f
--- /dev/null
+++ b/docs/components/demo/ColorSwatch/tailwind/tailwind.config.js
@@ -0,0 +1,6 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./**/*.vue'],
+ theme: {},
+ plugins: [],
+}
diff --git a/docs/components/demo/ColorSwatchPicker/css/index.vue b/docs/components/demo/ColorSwatchPicker/css/index.vue
new file mode 100644
index 0000000000..67a8f1d194
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/css/index.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatchPicker/css/styles.css b/docs/components/demo/ColorSwatchPicker/css/styles.css
new file mode 100644
index 0000000000..c927379b69
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/css/styles.css
@@ -0,0 +1,32 @@
+.root {
+ display: flex;
+ gap: 8px;
+}
+
+.item {
+ position: relative;
+}
+
+.swatch {
+ height: 32px;
+ width: 32px;
+ border-radius: 4px;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.dark .swatch {
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
+}
+
+.indicator {
+ position: absolute;
+ bottom: 4px;
+ right: 4px;
+ color: white;
+}
+
+.swatch[data-color-contrast='dark'] ~ .indicator {
+ color: black;
+}
\ No newline at end of file
diff --git a/docs/components/demo/ColorSwatchPicker/tailwind/index.vue b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
new file mode 100644
index 0000000000..cccced8106
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/tailwind/index.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js b/docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js
new file mode 100644
index 0000000000..fe691bdd5f
--- /dev/null
+++ b/docs/components/demo/ColorSwatchPicker/tailwind/tailwind.config.js
@@ -0,0 +1,6 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./**/*.vue'],
+ theme: {},
+ plugins: [],
+}
diff --git a/docs/content/docs/components/color-swatch-picker.md b/docs/content/docs/components/color-swatch-picker.md
new file mode 100644
index 0000000000..092b1da05f
--- /dev/null
+++ b/docs/content/docs/components/color-swatch-picker.md
@@ -0,0 +1,90 @@
+---
+
+title: Color Swatch Picker
+description: A component that allows users to select from a set of predefined colors, often used for themes or branding.
+name: color-swatch-picker
+---
+
+# Color Swatch Picker
+
+
+A component that allows users to select from a set of predefined colors, often used for themes or branding.
+
+
+
+
+## Features
+
+
+
+## Installation
+
+Install the component from your command line.
+
+
+
+## Anatomy
+
+Import all parts and piece them together.
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## API Reference
+
+### ColorSwatchPickerRoot
+
+The main component that displays a color swatch picker.
+
+
+
+### ColorSwatchPickerItem
+
+The item that represents a selectable color swatch.
+
+
+
+### ColorSwatchPickerItemSwatch
+
+The component that displays the color swatch within an item.
+
+
+
+### ColorSwatchPickerItemIndicator
+
+The component that indicates the selected color swatch within an item.
+
+
diff --git a/docs/content/docs/components/color-swatch.md b/docs/content/docs/components/color-swatch.md
new file mode 100644
index 0000000000..e91c869807
--- /dev/null
+++ b/docs/content/docs/components/color-swatch.md
@@ -0,0 +1,54 @@
+---
+
+title: Color Swatch
+description: A predefined color block that allows users to quickly select commonly used or brand-specific colors.
+name: color-swatch
+---
+
+# Color Swatch
+
+
+Displays a color swatch, which can be used to represent colors in a UI.
+
+
+
+
+## Features
+
+
+
+## Installation
+
+Install the component from your command line.
+
+
+
+## Anatomy
+
+Import all parts and piece them together.
+
+```vue
+
+
+
+
+
+```
+
+## API Reference
+
+### ColorSwatch
+
+The main component that displays a color swatch.
+
+
diff --git a/docs/content/meta/ColorSwatch.md b/docs/content/meta/ColorSwatch.md
new file mode 100644
index 0000000000..6ff30b9e57
--- /dev/null
+++ b/docs/content/meta/ColorSwatch.md
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerItem.md b/docs/content/meta/ColorSwatchPickerItem.md
new file mode 100644
index 0000000000..965b0b595c
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerItem.md
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerItemIndicator.md b/docs/content/meta/ColorSwatchPickerItemIndicator.md
new file mode 100644
index 0000000000..5df63e84f6
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerItemIndicator.md
@@ -0,0 +1,17 @@
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerItemSwatch.md b/docs/content/meta/ColorSwatchPickerItemSwatch.md
new file mode 100644
index 0000000000..701ee1d389
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerItemSwatch.md
@@ -0,0 +1,23 @@
+
+
+
diff --git a/docs/content/meta/ColorSwatchPickerRoot.md b/docs/content/meta/ColorSwatchPickerRoot.md
new file mode 100644
index 0000000000..55c54316ae
--- /dev/null
+++ b/docs/content/meta/ColorSwatchPickerRoot.md
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
diff --git a/packages/core/constant/components.ts b/packages/core/constant/components.ts
index e1f1ac71a8..80fa341e35 100644
--- a/packages/core/constant/components.ts
+++ b/packages/core/constant/components.ts
@@ -56,6 +56,17 @@ export const components = {
'CollapsibleContent',
] as const,
+ colorSwatch: [
+ 'ColorSwatch',
+ ] as const,
+
+ colorSwatchPicker: [
+ 'ColorSwatchPickerRoot',
+ 'ColorSwatchPickerItem',
+ 'ColorSwatchPickerItemSwatch',
+ 'ColorSwatchPickerItemIndicator',
+ ] as const,
+
combobox: [
'ComboboxRoot',
'ComboboxInput',
diff --git a/packages/core/src/ColorSwatch/ColorSwatch.test.ts b/packages/core/src/ColorSwatch/ColorSwatch.test.ts
new file mode 100644
index 0000000000..4a24d77de3
--- /dev/null
+++ b/packages/core/src/ColorSwatch/ColorSwatch.test.ts
@@ -0,0 +1,16 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+import { axe } from 'vitest-axe'
+import ColorSwatch from './story/_ColorSwatch.vue'
+
+describe('given a default ColorSwatch', () => {
+ it('should pass axe accessibility tests', async () => {
+ const wrapper = mount(ColorSwatch, { attachTo: document.body })
+ expect(await axe(wrapper.element)).toHaveNoViolations()
+ })
+
+ it('should render ColorSwatch', () => {
+ const wrapper = mount(ColorSwatch)
+ expect(wrapper.element).toBeTruthy()
+ })
+})
diff --git a/packages/core/src/ColorSwatch/ColorSwatch.vue b/packages/core/src/ColorSwatch/ColorSwatch.vue
new file mode 100644
index 0000000000..4d465c8ae9
--- /dev/null
+++ b/packages/core/src/ColorSwatch/ColorSwatch.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatch/index.ts b/packages/core/src/ColorSwatch/index.ts
new file mode 100644
index 0000000000..db424fa21e
--- /dev/null
+++ b/packages/core/src/ColorSwatch/index.ts
@@ -0,0 +1,4 @@
+export {
+ default as ColorSwatch,
+ type ColorSwatchProps,
+} from './ColorSwatch.vue'
diff --git a/packages/core/src/ColorSwatch/story/_ColorSwatch.vue b/packages/core/src/ColorSwatch/story/_ColorSwatch.vue
new file mode 100644
index 0000000000..b9eb415a17
--- /dev/null
+++ b/packages/core/src/ColorSwatch/story/_ColorSwatch.vue
@@ -0,0 +1,16 @@
+
+
+
+ />
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts b/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
new file mode 100644
index 0000000000..6798ee7ee5
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPicker.test.ts
@@ -0,0 +1,29 @@
+import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
+import { mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it } from 'vitest'
+import { axe } from 'vitest-axe'
+import ColorSwatchPicker from './story/_ColorSwatchPicker.vue'
+
+describe('given default ColorSwatchPicker', () => {
+ let wrapper: VueWrapper>
+ let content: DOMWrapper
+ let items: DOMWrapper[]
+
+ beforeEach(() => {
+ document.body.innerHTML = ''
+ wrapper = mount(ColorSwatchPicker, { attachTo: document.body })
+ content = wrapper.find('[role=listbox]')
+ items = wrapper.findAll('[role=option]')
+ })
+
+ it('should render the component', () => {
+ expect(wrapper.exists()).toBe(true)
+ expect(content.exists()).toBe(true)
+ expect(items.length).toBeGreaterThan(0)
+ })
+
+ it('should have no accessibility violations', async () => {
+ const results = await axe(wrapper.element)
+ expect(results).toHaveNoViolations()
+ })
+})
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
new file mode 100644
index 0000000000..53ed0e3092
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItem.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue
new file mode 100644
index 0000000000..7294b6935e
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemIndicator.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue
new file mode 100644
index 0000000000..189daffa4d
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerItemSwatch.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
new file mode 100644
index 0000000000..42d3423560
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/ColorSwatchPickerRoot.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/ColorSwatchPicker/index.ts b/packages/core/src/ColorSwatchPicker/index.ts
new file mode 100644
index 0000000000..9320ca4703
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/index.ts
@@ -0,0 +1,17 @@
+export {
+ default as ColorSwatchPickerItem,
+ type ColorSwatchPickerItemProps,
+} from './ColorSwatchPickerItem.vue'
+export {
+ default as ColorSwatchPickerItemIndicator,
+ type ColorSwatchPickerItemIndicatorProps,
+} from './ColorSwatchPickerItemIndicator.vue'
+export {
+ default as ColorSwatchPickerItemSwatch,
+ type ColorSwatchPickerItemSwatchProps,
+} from './ColorSwatchPickerItemSwatch.vue'
+export {
+ default as ColorSwatchPickerRoot,
+ type ColorSwatchPickerRootEmits,
+ type ColorSwatchPickerRootProps,
+} from './ColorSwatchPickerRoot.vue'
diff --git a/packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue b/packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue
new file mode 100644
index 0000000000..65f562fc6f
--- /dev/null
+++ b/packages/core/src/ColorSwatchPicker/story/_ColorSwatchPicker.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/src/color/index.ts b/packages/core/src/color/index.ts
new file mode 100644
index 0000000000..9c56149efa
--- /dev/null
+++ b/packages/core/src/color/index.ts
@@ -0,0 +1 @@
+export * from './utils'
diff --git a/packages/core/src/color/utils.ts b/packages/core/src/color/utils.ts
new file mode 100644
index 0000000000..51ee2265dc
--- /dev/null
+++ b/packages/core/src/color/utils.ts
@@ -0,0 +1,129 @@
+/**
+ * Converts a hex color string to RGB (Red, Green, Blue).
+ * @param hex
+ * @returns
+ */
+export function hexToRGB(hex: string): { r: number, g: number, b: number } {
+ hex = hex.replace(/^#/, '')
+ const bigint = parseInt(hex, 16)
+ const r = (bigint >> 16) & 255
+ const g = (bigint >> 8) & 255
+ const b = bigint & 255
+ return { r, g, b }
+}
+
+/**
+ * Converts a hex color string to HSL (Hue, Saturation, Lightness).
+ * @param hex Hex color string (e.g., "#ff5733")
+ * @returns An object containing hue, saturation, and lightness values.
+ */
+export function hexToHSL(hex: string): { h: number, s: number, l: number } {
+ let { r, g, b } = hexToRGB(hex)
+
+ r /= 255
+ g /= 255
+ b /= 255
+
+ const max = Math.max(r, g, b)
+ const min = Math.min(r, g, b)
+ let h: number
+ let s: number
+ let l: number = (max + min) / 2
+
+ if (max === min) {
+ h = s = 0 // achromatic
+ }
+ else {
+ const d = max - min
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
+ if (max === r) {
+ h = (g - b) / d + (g < b ? 6 : 0)
+ }
+ else if (max === g) {
+ h = (b - r) / d + 2
+ }
+ else {
+ h = (r - g) / d + 4
+ }
+ h /= 6
+ h *= 360
+ s *= 100
+ l *= 100
+ }
+
+ return { h, s, l }
+}
+
+/**
+ * Converts a hex color string to a human-readable color name.
+ * @param hex Hex color string (e.g., "#ff5733")
+ * @returns A human-readable color name based on the hue, saturation, and lightness.
+ */
+export function getColorName(hex: string) {
+ const { h, s, l } = hexToHSL(hex)
+
+ // Handle achromatic colors (low saturation)
+ if (s < 0.1) {
+ if (l < 0.1)
+ return 'black'
+ if (l > 0.95)
+ return 'white'
+ if (l < 0.2)
+ return 'very dark gray'
+ if (l < 0.35)
+ return 'dark gray'
+ if (l < 0.65)
+ return 'gray'
+ if (l < 0.8)
+ return 'light gray'
+ return 'very light gray'
+ }
+
+ // Determine base color by hue
+ let baseName
+ if (h < 15 || h >= 345)
+ baseName = 'red'
+ else if (h < 45)
+ baseName = 'orange'
+ else if (h < 75)
+ baseName = 'yellow'
+ else if (h < 105)
+ baseName = 'yellow-green'
+ else if (h < 135)
+ baseName = 'green'
+ else if (h < 165)
+ baseName = 'green-cyan'
+ else if (h < 195)
+ baseName = 'cyan'
+ else if (h < 225)
+ baseName = 'blue'
+ else if (h < 255)
+ baseName = 'blue-violet'
+ else if (h < 285)
+ baseName = 'violet'
+ else if (h < 315)
+ baseName = 'magenta'
+ else baseName = 'red-magenta'
+
+ // Add descriptors based on saturation and lightness
+ const descriptors = []
+ if (s > 0.8)
+ descriptors.push('vibrant')
+ else if (s < 0.3)
+ descriptors.push('muted')
+
+ if (l > 0.8)
+ descriptors.push('light')
+ else if (l < 0.3)
+ descriptors.push('dark')
+
+ return descriptors.length > 0
+ ? `${descriptors.join(' ')} ${baseName}`
+ : baseName
+}
+
+export function getColorContrast(hex: string): 'light' | 'dark' {
+ const { r, g, b } = hexToRGB(hex)
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
+ return luminance > 0.5 ? 'dark' : 'light'
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 348691136c..5546c9b3f7 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -5,6 +5,8 @@ export * from './Avatar'
export * from './Calendar'
export * from './Checkbox'
export * from './Collapsible'
+export * from './ColorSwatch'
+export * from './ColorSwatchPicker'
export * from './Combobox'
// utilities
export * from './ConfigProvider'