npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ldc-color-picker

v0.6.0

Published

A dependency-free React color picker: 12-swatch palette, HSV surface with HEX/RGB/HSL inputs, and pluggable saved colors. Themes off Ionic when it's there, stands alone when it isn't.

Downloads

367

Readme

ldc-color-picker

A React color picker with no dependencies but React: a 12-swatch palette, an HSV surface with HEX/RGB/HSL inputs, and saved colors you can persist wherever you like.

It themes off Ionic when Ionic is there, and stands alone when it isn't.

Extracted from notant, where it shipped as NotantColorPicker in 0.6.0.

Install

npm install ldc-color-picker

Use

import { useState } from 'react';
import { ColorPicker } from 'ldc-color-picker';
import 'ldc-color-picker/styles.css';

function PenTool() {
  const [color, setColor] = useState('#e5484d');
  return <ColorPicker value={color} onChange={setColor} label="Pen color" />;
}

Styling, in four layers

The styles arrive with the component. Since 0.6.0 there is nothing to remember:

import { ColorPicker } from 'ldc-color-picker';   // that is all

The stylesheet is injected on first mount, as the first stylesheet in <head> — which is the point. Every rule in it therefore loses a specificity tie to anything your app writes later, so your CSS overrides it exactly as it did when the import was manual.

ldc-color-picker/styles.css still exists and still works. Import it when you want the CSS in your own pipeline, in a <link>, or ahead of first paint — the injected copy loses to it, being earlier. And opt out of the built-in look entirely with:

<html data-ldc-color-picker="no-styles">

| layer | scope | beats | | --- | --- | --- | | props | one instance — written inline | everything | | your CSS | your app | the stylesheet (single-class selectors, 0,1,0) | | --ldc-* variables | one origin | read by the stylesheet | | the stylesheet | the default look | — |

A prop you do not pass writes no declaration at all. That is the rule the whole arrangement rests on: an inline value is one your own CSS can never override again, so options stay silent until asked for. Reach for variables to theme an app, and props only where one call site genuinely differs.

Options

| Prop | Type | Default | What it does | | --- | --- | --- | --- | | value | string | — | Current color. Any hex form; compared after normalizing. | | onChange | (value: string) => void | — | Called with a normalized #rrggbb. | | swatches | readonly string[] | 12 built-ins | Replaces the palette. | | label | string | 'Color' | Accessible name. Say what it colors. | | disabled | boolean | false | Grays out the trigger. | | allowCustom | boolean | true | false hides the HSV surface, leaving only swatches. | | favorites | FavoritesStore \| false | localStorage | Where saved colors live. false drops the feature. | | scope | string | unset | Namespace for the default store. Omit and every picker on the origin shares one list. | | side | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Preferred side. Flips when there is no room. | | align | 'start' \| 'center' \| 'end' | 'center' | Alignment along the cross axis. | | border | string \| BorderOptions | 1px hairline | The trigger's border. A shorthand, or { width, style, color } — the same type panel.border takes. Unlike the panel it has one already, so this overrides; { style: 'none' } removes it. | | className | string | — | Extra class on the trigger. | | panel | PanelOptions | unset | How the panel looks — see below. Every field optional; every one omitted leaves that rule with the stylesheet. | | open | boolean | unset | Controls whether the panel is showing. Omit it and the picker decides, as before. Passing it also switches the panel from popover="auto" to popover="manual", so the browser stops dismissing it and you do — see below. | | onOpenChange | (open: boolean) => void | — | The picker asking to be opened or closed. Fires in both modes. |

Styling the panel

Until now the panel's box was fixed in the stylesheet and its colors reachable only through the --ldc-* chain — which is per-origin, so two pickers on one page could not look different. panel is per-instance.

| Field | Type | Default | | | --- | --- | --- | --- | | width | number \| string | --ldc-color-panel-width, 236px | Beats the custom property; an inline declaration always does. | | padding | 'sm' \| 'md' \| 'lg' \| number \| string | 10px | Presets are 6 / 10 / 16, so md is the default unchanged. | | radius | 'sm' \| 'md' \| 'lg' \| number \| string | 12px | Presets are 6 / 12 / 20. | | border | string \| PanelBorderOptions | none — border: 0 | A shorthand, or { width, style, color }. See below. The shadow is what separates the panel from the page; turn the shadow off and it has nothing. | | background | string | --ldc-color-surface | | | color | string | --ldc-color-ink | | | shadow | 'sm' \| 'md' \| 'lg' \| string \| false | md | false writes box-shadow: none. | | maxHeight | number \| string | min(360px, 78vh) | Expanding Custom nearly doubles the panel; the cap is why it does not run off a short window. | | backdrop | string | transparent | Only paints while the panel is an auto popover — a controlled one is manual and gets no backdrop. | | gap | 'sm' \| 'md' \| 'lg' \| number | 8 | Space between trigger and panel. Presets are 4 / 8 / 16, so md is the default unchanged. | | viewportMargin | 'sm' \| 'md' \| 'lg' \| number | 8 | How close the panel may get to the viewport edge. Same presets. |

<ColorPicker
  value={color}
  onChange={setColor}
  panel={{ radius: 'lg', shadow: false, border: '1px solid #e4e4e7', padding: 14 }}
/>

The hex field's typography

hexField could always express its geometry from props — width, radius — and never its typography. Matching an existing field meant writing CSS against .ldc-color-hex internals, which is not an API.

| Field | Type | Default | | --- | --- | --- | | padding | 'sm' \| 'md' \| 'lg' \| number \| string | 7px 9px, which is md. A number is every side; a string is verbatim, so .5rem .75rem works. | | fontSize | number \| string | 13px | | fontFamily | string | inherit. monospace is the one worth knowing about — proportional digits make #ffffff and #000000 different widths, so a column of fields never lines up. |

<ColorPicker
  value={color}
  onChange={setColor}
  hexField={{ editable: true, fontFamily: 'monospace', fontSize: 14, padding: '.5rem .75rem' }}
/>

Padding reaches the readout through a custom property rather than an inline style, so the copy button's reserved space survives it. An inline padding shorthand would beat that rule and the value would run underneath the button.

The border, in parts

Both border props — the trigger's and panel.border — take a CSS shorthand, or the three pieces separately, using one shared BorderOptions type:

| Field | Type | Default once any part is set | | --- | --- | --- | | width | 'sm' \| 'md' \| 'lg' \| number \| string | 1px. Presets are 1 / 2 / 4. | | style | 'solid' \| 'dashed' \| 'dotted' \| 'double' \| 'none' | solid. double needs 3px+ to read as two lines. | | color | string | the same hairline the swatches use, so it themes with them | | position | 'inside' \| 'outside' | inside |

position is box-sizing, and there is no center. CSS has no border-alignment property — CSS.supports('border-alignment', …) is false, as is border-position. A border always sits in the same place in the box model; what position changes is whether the declared size includes it. inside (border-box) is what both halves do today: a 4px border eats inward and neither element grows, so thickening one never moves your layout. outside (content-box) makes the declared size the content size and the element grows by the border on each side. A true center would need the border drawn as an outline with a negative outline-offset, and outline is the focus ring on the trigger and every swatch.

The two differ only in where they start. The panel has no border; the trigger has a 1px hairline — thin on purpose, so a white swatch stays visible without a ring competing with the colour it contains. { style: 'none' } is how that hairline comes off, and the reason none is in the union at all.

<ColorPicker value={color} onChange={setColor} panel={{ border: { style: 'dashed' } }} />
// -> 1px dashed <hairline>

The parts exist because in practice you change one of them — thicker, or dashed — and a shorthand makes you restate the other two to do it. That is how a border's colour quietly stops matching the theme. An empty object means the same as no border at all: nothing is written, and the stylesheet's border: 0 stands.

Omitting a field is not the same as passing its default. Nothing is written inline for a field you leave out, so the stylesheet keeps that rule and a host's own CSS can still override it. Pass shadow: 'md' and you get the same shadow — but now as an inline declaration that host CSS can no longer beat.

Holding the panel open

open is not a hint the browser can overrule. An uncontrolled panel is a native popover="auto", and light dismiss is the platform's — the next click anywhere outside closes it no matter what React thinks. So passing open switches the panel to popover="manual", which hands dismissal to you:

// Stays up until you say otherwise. Escape, an outside click and picking a
// color all still ask, through onOpenChange; nothing happens unless you act.
<ColorPicker value={color} onChange={setColor} open onOpenChange={() => {}} />

The ordinary controlled case is the same shape, honoured rather than ignored:

<ColorPicker value={color} onChange={setColor} open={open} onOpenChange={setOpen} />

Theming

Every color resolves through a three-step chain:

var(--ldc-color-accent, var(--ion-color-primary, #0054e9))

Set --ldc-color-accent to theme it deliberately. Set nothing and, inside an Ionic app, it picks up the host theme on its own. Set nothing anywhere and it still looks like something.

| Property | Falls back to | Then | | --- | --- | --- | | --ldc-color-accent | --ion-color-primary | #0054e9 | | --ldc-color-surface | --ion-background-color | #fff | | --ldc-color-ink | --ion-text-color | inherit | | --ldc-color-saved | --ion-color-warning | #e0a800 | | --ldc-color-hairline | — | rgba(0,0,0,.22) | | --ldc-color-panel-width | — | 236px |

Saved colors

favorites takes anything with get and set:

<ColorPicker
  value={color}
  onChange={setColor}
  favorites={{
    get: () => fetch('/api/me/colors').then((r) => r.json()),
    set: (colors) => fetch('/api/me/colors', { method: 'PUT', body: JSON.stringify(colors) }),
  }}
/>

The default is localStorage under one unscoped key, so two pickers on a page share a list — one person, one set of colors. Pass scope when that is wrong.

The panel is a native popover

It renders in the top layer, so no ancestor's overflow: hidden can clip it, and the browser handles light dismiss, Escape and focus. Placement is ours — computePlacement is exported and tested on its own.

Browsers without the popover API fall back to a fixed-position panel with our own dismiss handling.

License

PolyForm Noncommercial 1.0.0 — see LICENSE.md.