rn-markdown-editor: ColorPickersteemCreated with Sketch.

in Steem Devyesterday

ColorPicker

A full HSVA color picker for Expo / React Native (iOS, Android, Web) — a hex-text-field-plus-swatch trigger that opens a saturation/value area, hue slider, alpha slider, an optional web eyedropper, and a live preview swatch, in either a popover (web) or bottom sheet (native).

import { ColorPicker } from "rn-markdown-editor/components";

<ColorPicker
  defaultValue="#EB5E41"
  onValueChange={({ valueAsString }) => console.log(valueAsString)}
/>

<ColorPicker
  value={hex}
  format="hsla"
  onValueChange={({ valueAsString }) => setHex(valueAsString)}
  onValueChangeEnd={({ valueAsString }) => save(valueAsString)}
/>

How it works internally

HSVA as the single source of truth. Every input format the picker accepts — #hex/#hexa, rgb()/rgba(), hsl()/hsla(), hsb()/hsba() — is parsed by parseColorString down into one internal HSVA shape (h 0–360, s/v 0–100, a 0–1). Output works the same way in reverse: a ColorPickerColorValue object built by createColorValue exposes toHex, toHexAlpha, toRgba, toHsla, and a format-aware toString, so the picker only ever does color math once regardless of which format the consumer wants back.

Controllable everything. Value, format, and open state each run through the same useControllableState hook used by Select/Slider — pass value/format/open to fully control them, or defaultValue/defaultFormat/defaultOpen to let the component own its own state, with onValueChange/onFormatChange/onOpenChange firing either way.

Drag vs. commit are separate states. Dragging the SV area or a slider updates a local dragHsva on every pointer move (so the swatch and thumbs track the finger in real time) while commit decides whether that also propagates outward: with immediate (default true) every drag tick calls onValueChange; with immediate={false}, onValueChange only fires once on release, though onValueChangeEnd always fires on release regardless. This mirrors why intermediate values while typing a number are never clamped — you don't want to fight the user mid-gesture.

One drag surface, one slider primitive, reused. SVArea is the 2D saturation/value pad: a View with raw onResponderGrant/Move/Release handlers (not PanResponder) that converts a touch's locationX/Y into s/v percentages against its own measured onLayout size. ChannelSlider is the same idea flattened to one axis, and powers both the hue and alpha sliders — hue supplies a rainbow LinearGradient as its background, alpha supplies a Checkerboard layered under a transparent→opaque gradient of the current color, and both drive an identical Thumb.

Checkerboard for translucency. Anywhere alpha needs to be visible against the UI (alpha slider track, preview swatch), a small Svg Pattern tiles a 2×2 gray/white checker at a configurable cell size — cheaper and crisper than a bitmap asset at arbitrary sizes.

Same popover-positioning logic as the rest of the library. On web, the panel is measured via containerRef.measureInWindow, then flipped above the trigger if there isn't enough room below and clamped horizontally so it never overflows the viewport — the exact same measure→flip→clamp approach DropdownMenuContent uses. Native platforms skip all of that and get a bottom-sheet Modal instead; both shells render the identical panel tree, so there's no behavioral drift between platforms.

Eyedropper is feature-detected, not platform-detected. showEyeDropper only actually renders as usable when Platform.OS === "web" and "EyeDropper" in globalThis — Safari and Firefox don't ship the API yet, so the button still renders (for layout stability) but is disabled rather than assumed-available.

Hidden form input on web. When name is supplied, a real DOM <input type="hidden"> is rendered alongside the RN tree with the current toString(format) value, so the picker can participate in a native HTML form submit on web without any extra wiring.

Key props

PropTypeDefaultDescription
value / defaultValuestringControlled/uncontrolled color, any accepted format (#hex, rgb(), hsl(), hsb())
onValueChange / onValueChangeEnd(details) => voidFires with { value, valueAsString } on drag / on drag release respectively
immediatebooleantruefalse defers onValueChange until the interaction ends
format / defaultFormatColorPickerFormat"rgba"hex \rgba \hsla \hsba — shape of the emitted string
open / defaultOpen / onOpenChangeControlled/uncontrolled popover-modal open state
inlinebooleanfalseRenders the picker panel directly with no trigger or popover shell
showEyeDropperbooleantrueShows the eyedropper button (web-only, auto-disabled where the browser API is absent)
disabled / readOnly / required / invalidbooleanStandard field states; invalid tints the hex field border
name / formstringWeb-only hidden <input> for native HTML form submission
size`"sm" \"md" \"lg"`"md"Trigger control sizing (height, font size, swatch size)
variant`"outline" \"filled" \"flushed" \"unstyled"`"outline"Trigger control chrome
positioningColorPickerPositioningplacement (top/bottom/auto), offset, sameWidth for the web popover
idsColorPickerIdsPer-part DOM/testID overrides (root, trigger, area, hueSlider, alphaSlider, …)

ColorSwatch

Tags: color · display · selection

A single pressable color chip, plus a ColorSwatchGroup for rendering a row (or wrapped grid) of them — the building block used for palette pickers, theme selectors, and tag/label color choices.

import { ColorSwatch, ColorSwatchGroup } from "rn-markdown-editor/components";

<ColorSwatch color="#3B82F6" selected onPress={(c) => console.log(c)} />

<ColorSwatch colorKey="destructive" variant="circle" label="Danger" />

<ColorSwatchGroup
  palette="rainbow"
  mix
  mixSteps={5}
  selectedColor={selected}
  onSelectColor={setSelected}
  allowCustom
  onCustomPress={openPicker}
/>

How it works internally

Three ways to source a color, one precedence order. ColorSwatch resolves its displayed color as custom ?? color ?? colors[colorKey] ?? config?.fallbackColor ?? colors.border inside a useMemo — an explicit one-off custom value (e.g. handed back from a ColorPicker) always wins over a static color prop, which wins over a themed colorKey lookup, so the same component works equally well for "here's an exact color" and "give me the theme's destructive swatch" call sites.

Type changes what's drawn, not just how it looks. type="solid" fills the shape with the resolved color; type="outline" makes the background transparent and uses the color for the border/ring instead (so an "outline destructive" swatch reads as a border, not a filled block); type="gradient" layers a slightly lighter top border on top of a solid fill via shadeColor, cheaply faking a top-lit sheen without an actual gradient image.

Size accepts a preset or a raw number. resolveSize looks a named size (xs/sm/default/lg/xl) up in a module-level SIZE_DIMENSIONS map (with theme config.sizes overrides taking precedence), but falls straight through to whatever number you pass — so size={20} and size="sm" (which resolves to 24) are both valid without any special-casing in the component.

Variant is a pure function of size, not a lookup table. VARIANT_RADIUS maps each SwatchVariant (rounded/square/circle) to a function of the resolved pixel dimension rather than a fixed radius — rounded is max(4, dim * 0.25) and circle is always dim / 2 — so a swatch keeps proportionally correct corners whether it's rendered at 16px or 64px.

Selection state is a shadow + checkmark, not a second border color. selected adds a colored shadowColor/shadowRadius glow in the swatch's own color (rather than swapping the border to a generic "selected" tint) and overlays a Text whose own color is chosen by type — resolvedColor itself when outline, or the theme's primaryForeground otherwise, so the checkmark stays legible against either a filled chip or a transparent-background outline chip.

ColorSwatchGroup composes ramps, it doesn't fetch them. Passing mix doesn't add extra swatches from anywhere external — generateMix takes each base color and walks mixSteps evenly-spaced lightness deltas (default spread of 36, i.e. ±18) through shadeColor, which itself works by parsing the color to HSL (parseHsl for hsl()/hsla() strings, hexToHsl for hex) and just nudging the l channel — so a mix ramp is always derived math on the input colors, never a separate palette asset.

Named palettes are theme lookups, "rainbow" is the one static exception. resolvePalette maps "brand"/"status"/"grayscale" to specific keys pulled live off the active ThemeColors (so they re-theme automatically), while "rainbow" returns a fixed six-hue array module constant, since a hue wheel has no sensible "theme" version.

Key props — ColorSwatch

PropTypeDefaultDescription
colorstringExplicit hex/rgb/hsl color. Highest precedence after custom
colorKeykeyof ThemeColorsPulls a color straight from the active theme (e.g. "destructive")
customstringOne-off override (e.g. from a paired ColorPicker); takes precedence over both
sizeSwatchSize"default"xs \sm \default \lg \xl \raw number
variantSwatchVariant"rounded"rounded \square \circle — shape
typeSwatchType"solid"solid \outline \gradient — fill style
selectedbooleanfalseAdds a colored glow + checkmark overlay
labelstringOptional caption rendered under the swatch
onPress(color: string) => voidCalled with the swatch's resolved color string

Key props — ColorSwatchGroup

PropTypeDefaultDescription
colorsstring[]Explicit list of colors to render
palette`"brand" \"status" \"grayscale" \"rainbow" \string[]`Named theme palette (ignored if colors is set)
mixbooleanfalseExpands each base color into a light→dark ramp instead of rendering as-is
mixStepsnumber5Steps generated per color when mix is enabled
gapnumber8Spacing between swatches, in px
wrapbooleanfalseWraps onto multiple lines instead of a single row
selectedColor / onSelectColorControlled selection across the group
allowCustom / onCustomPressAdds a trailing dashed "+" swatch that opens a picker

Coin Marketplace

STEEM 0.04
TRX 0.33
JST 0.104
BTC 64621.25
ETH 1928.65
USDT 1.00
SBD 0.37