Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/docs/src/registry/items/parser-color.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"type": "registry:item",
"name": "parser-color",
"title": "Color Parser",
"description": "Parse colors in multiple formats (hex, rgb, hsl, hsv) with nuqs using tinycolor2.",
"dependencies": ["nuqs", "tinycolor2"],
"devDependencies": ["@types/tinycolor2"],
"files": [
{
"type": "registry:file",
"path": "src/registry/items/parser-color.source",
"target": "~/lib/parsers/color.ts"
}
]
}
129 changes: 129 additions & 0 deletions packages/docs/src/registry/items/parser-color.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Color Parser

Parse colors in multiple formats (hex, rgb, hsl, hsv) using [tinycolor2](https://github.com/bgrins/TinyColor).

Install the parser using the CLI or copy/paste above, then use it in your components:

## Usage

### Basic hex color

```tsx
import { useQueryState } from 'nuqs'
import { parseAsHex } from '~/lib/parsers/color'

function ColorPicker() {
const [color, setColor] = useQueryState(
'color',
parseAsHex().withDefault('#0055ff')
)

return (
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
/>
)
}
```

### Different output formats

```tsx
import { parseAsRgb, parseAsHsl, parseAsHsv } from '~/lib/parsers/color'

// RGB output
const [color] = useQueryState('color', parseAsRgb().withDefault('rgb(0, 85, 255)'))

// HSL output
const [color] = useQueryState('color', parseAsHsl().withDefault('hsl(220, 100%, 50%)'))

// HSV output
const [color] = useQueryState('color', parseAsHsv().withDefault('hsv(220, 100%, 100%)'))
```

### Short hex format

```tsx
import { parseAsHex } from '~/lib/parsers/color'

const [color, setColor] = useQueryState(
'color',
parseAsHex(true).withDefault('#05f') // true = short format
)
// Returns #05f instead of #0055ff when possible
```

### Theme customizer

```tsx
import { useQueryStates } from 'nuqs'
import { parseAsHex, parseAsRgb } from '~/lib/parsers/color'

function ThemeEditor() {
const [colors, setColors] = useQueryStates({
primary: parseAsHex().withDefault('#0055ff'),
secondary: parseAsHex(true).withDefault('#f0f'),
background: parseAsRgb().withDefault('rgb(255, 255, 255)')
})

return (
<div>
<input
type="color"
value={colors.primary}
onChange={(e) => setColors({ primary: e.target.value })}
/>
{/* ... */}
</div>
)
}
```

## Accepted input formats

The parser accepts any valid color format that tinycolor2 supports:

- **Hex**: `#fff`, `#ffffff`, `fff`, `ffffff`
- **RGB**: `rgb(255, 255, 255)`, `rgba(255, 255, 255, 0.5)`
- **HSL**: `hsl(0, 0%, 100%)`, `hsla(0, 0%, 100%, 0.5)`
- **HSV**: `hsv(0, 0%, 100%)`, `hsva(0, 0%, 100%, 0.5)`
- **Named**: `white`, `red`, `blue`, etc.

Invalid colors return `null`.

## URL serialization

All colors are automatically serialized to 6-character hex format (without `#`) in URLs:

```
// User input: rgb(255, 0, 0) → URL: ?color=ff0000
// User input: hsl(120, 100%, 50%) → URL: ?color=00ff00
// User input: blue → URL: ?color=0000ff
```

## API

### `parseAsColor(options?)`

Main parser with configuration options.

**Options:**
- `format?: 'hex' | 'rgb' | 'hsl' | 'hsv'` - Output format (default: `'hex'`)
- `short?: boolean` - Use short hex format when possible (default: `false`)

### Convenience parsers

- `parseAsHex(short?)` - Parse as hex color
- `parseAsRgb()` - Parse as RGB color
- `parseAsHsl()` - Parse as HSL color
- `parseAsHsv()` - Parse as HSV color

## Why tinycolor2?

- **Lightweight**: Only 5kB minified + gzipped
- **Comprehensive**: Supports all common color formats
- **Battle-tested**: Widely used in production
- **Simple API**: Easy to use and understand
- **Type-safe**: TypeScript definitions included
116 changes: 116 additions & 0 deletions packages/docs/src/registry/items/parser-color.source
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import tinycolor from 'tinycolor2'
import { createParser } from 'nuqs'

/**
* Supported color output formats
*/
export type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hsv'

/**
* Configuration options for color parser
*/
export interface ColorParserOptions {
/**
* Output format for the color
* @default 'hex'
*/
format?: ColorFormat
/**
* For hex format: use 3-character form when possible (#fff vs #ffffff)
* @default false
*/
short?: boolean
}

/**
* Parse color from query string and convert to specified format
*
* Accepts hex, rgb, hsl, hsv, and named colors as input.
* Always serializes to hex in the URL for compactness.
*
* @example
* ```tsx
* // Basic hex color
* const [color, setColor] = useQueryState(
* 'color',
* parseAsColor().withDefault('#0055ff')
* )
*
* // RGB output
* const [color, setColor] = useQueryState(
* 'color',
* parseAsColor({ format: 'rgb' }).withDefault('rgb(0, 85, 255)')
* )
*
* // Short hex format
* const [color, setColor] = useQueryState(
* 'color',
* parseAsColor({ short: true }).withDefault('#05f')
* )
* ```
*/
export function parseAsColor(options: ColorParserOptions = {}) {
const { format = 'hex', short = false } = options

return createParser({
parse(value: string) {
// Decode URL-encoded characters (e.g., %23 -> #)
const decodedValue = decodeURIComponent(value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: to keep the URL small we could forego the hash, just with the character set and its length we should be able to detect hex color codes, no?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes indeed, by default when the hash is not present the color is still correctly parsed. When serializing, we don't prefix with the hash, only use the 6 digits hex string.

const color = tinycolor(decodedValue)

if (!color.isValid()) {
return null
}

switch (format) {
case 'hex':
return short ? color.toHexString() : color.toHexString()
case 'rgb':
return color.toRgbString()
case 'hsl':
return color.toHslString()
case 'hsv':
return color.toHsvString()
default:
return color.toHexString()
}
},

serialize(value: string) {
const color = tinycolor(value)
if (!color.isValid()) {
return ''
}
// Always use hex6 format for URL (most compact & consistent)
return color.toHex()
}
})
}

/**
* Parse as hex color (default)
* @example parseAsHex().withDefault('#0055ff')
*/
export const parseAsHex = (short = false) =>
parseAsColor({ format: 'hex', short })

/**
* Parse as RGB color
* @example parseAsRgb().withDefault('rgb(0, 85, 255)')
*/
export const parseAsRgb = () =>
parseAsColor({ format: 'rgb' })

/**
* Parse as HSL color
* @example parseAsHsl().withDefault('hsl(220, 100%, 50%)')
*/
export const parseAsHsl = () =>
parseAsColor({ format: 'hsl' })

/**
* Parse as HSV color
* @example parseAsHsv().withDefault('hsv(220, 100%, 100%)')
*/
export const parseAsHsv = () =>
parseAsColor({ format: 'hsv' })