GrColorPicker

  • core
  • forms
  • API · 15 props
  • ⌨ keys

Picks a color for theme and branding settings: hue, saturation, lightness and opacity sliders, a hex field and a palette.

import { GrColorPicker } from '@feugene/granularity/components/GrColorPicker'

Overview

When to take it

  • the colour is set by the user — the theme of a brand, the label of a project, the colour of a category;
  • an exact code is needed — a hex field beside the channels: a colour is more often brought from a mock-up than picked by eye;
  • there is a house set — presets shows the palette people choose from in nine cases out of ten;
  • transparency is needed — alpha adds a channel and changes the format of the value.

When to take something else

NeedTake
A choice among several fixed coloursGrRadioGroup / GrSegmented
The colour is a component tone from the theme palettethe tone prop of the component in question
The value is an arbitrary stringGrInput

Variants every tone and view, rendered by the real component

view

  • sliders
  • area

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled
  • readonly
  • invalid

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • A brand colour that must stay readable

    The value is a hex string — the form CSS, tokens and the server all read. view="area" gives the saturation × lightness square, presets the brand palette, eyedropper a pipette where the browser has one, and name sends the value with a native form. White text needs 4.5:1, so a colour that fails is invalid and the line under it says why, while the preview shows where the colour lands.

    BrandingShown on invoices, payment pages and emails
    White text 5.5:1 — readable
    Acme BillingInvoice INV-0418
    Amount due€1,284.50
  • Label colours in a list

    A small pill picker fits a list row, and placement="bottom-end" opens the panel inwards from the right edge. The system label is readonly: the panel opens and shows the colour, but the value does not change.

    LabelsBilling web · project board
    • Bug · system
      Read only
    • Design
    • Backend
    • Waiting on customer
  • An overlay with transparency

    alpha adds the fourth slider and makes the value #RRGGBBAA, which CSS reads as is — the photo shows through the overlay and the headline stays readable. Presets carry their own transparency.

    Sign-in pageOverlay over the background photo
    Welcome back
    #0f172abf

API generated from the package · cannot drift

Props

PropTypedefaultDescription
placement"bottom-start" | "bottom-end" | "top-start" | "top-end" | undefined"bottom-start"The side the panel opens from.
disabledboolean | undefinedfalseThe field is unavailable: the trigger is disabled, the panel does not open. Also switched on by `disabled` of `GrFormField`.
openv-modelboolean | undefinedundefinedThe controlled state of the panel (`v-model:open`).
readonlyboolean | undefinedfalseRead-only: the colour is visible, the panel opens, but the value does not change.
invalidboolean | undefinedfalseThe error state: a border in the error colour and `aria-invalid`. Also switched on by an error of `GrFormField`.
requiredboolean | undefinedfalseA required field. `aria-required` is not valid on a trigger button, so the requirement goes into its description for a screen reader.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe height of the trigger and the scale of the panel on the control scale. Not set — from `GrConfigProvider`, otherwise `md`.
ariaLabelstring | undefinedundefinedThe accessible name of the trigger outside `GrFormField` — when there is no visible label nearby.
namestring | undefinedundefinedThe name for a native form: the value leaves in a hidden field.
shape"box" | "pill" | undefinedundefinedThe shape of the border. `box` — the rounding of the controls scale; `pill` — a pill.
alphaboolean | undefinedfalseA fourth slider and the eight-digit form of hex.
view"sliders" | "area" | undefinedundefinedHow the saturation and lightness are set: with two sliders or with a square. The hue and the opacity stay sliders in both views.
eyedropperboolean | undefinedundefinedThe eyedropper button. Drawn only where the browser provides `EyeDropper` — today that is Chromium.
presetsstring[] | undefined[]A palette for quick picking. If it is empty, the block is not rendered.

Events

EventSignatureDescription
update:modelValue(event: "update:modelValue", value: string): void—
change(event: "change", value: string): void—
update:open(event: "update:open", value: boolean): void—
focus(event: "focus", event: FocusEvent): void—
blur(event: "blur", event: FocusEvent): void—

Methods

MethodsSignatureDescription
focus() => voidFocus on the trigger button from code.
blur() => voidRemove focus from the trigger button.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrColorPickerConfigurableProps, GrColorPickerProps, GrColorPickerEmits } from '@feugene/granularity/components/GrColorPicker'

Filedist/types/components/GrColorPicker/config.d.ts

grColorPickerConfigconst
const grColorPickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrColorPicker">;

Filedist/types/components/GrColorPicker/defaults.d.ts

GrColorPickerConfigurablePropsinterface
interface GrColorPickerConfigurableProps {
    size: GrColorPickerSize;
    shape: GrControlShape;
    view: GrColorPickerView;
    eyedropper: boolean;
}

Filedist/types/components/GrColorPicker/GrColorPicker.vue.d.ts

GrColorPickerPropsinterface
interface GrColorPickerProps {
    modelValue: string;
    alpha?: boolean;
    view?: GrColorPickerView;
    eyedropper?: boolean;
    presets?: string[];
    size?: GrColorPickerSize;
    shape?: GrControlShape;
    open?: boolean;
    placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
    name?: string;
    disabled?: boolean;
    readonly?: boolean;
    invalid?: boolean;
    required?: boolean;
    ariaLabel?: string;
}
GrColorPickerEmitsinterface
interface GrColorPickerEmits {
    (e: 'update:modelValue', value: string): void;
    (e: 'change', value: string): void;
    (e: 'update:open', value: boolean): void;
    (e: 'focus', event: FocusEvent): void;
    (e: 'blur', event: FocusEvent): void;
}

Filedist/types/components/GrColorPicker/grColorPickerStyles.d.ts

GrColorPickerSizetype
type GrColorPickerSize = GrComponentSize;

Filedist/types/components/GrColorPicker/color.d.ts

GrHslatype
type GrHsla = {
    h: number;
    s: number;
    l: number;
    a: number;
};

Filedist/types/components/GrColorPicker/safelist.d.ts

grColorPickerSafelistconst
const grColorPickerSafelist: string[];

Filedist/types/components/GrColorPicker/index.d.ts

GrColorPickerInstancetype
type GrColorPickerInstance = ComponentExposed<typeof GrColorPickerComponent>;

Accessibility

APG pattern

dialog + slider

Keys

EscTab↑ / ↓

Usage from the library prose

The model is a hex string

modelValue is #RRGGBB, and with alpha it is #RRGGBBAA. That is the form in which a colour lies in the theme tokens, arrives from the backend and is understood by CSS: the consumer has to convert neither on the way in nor on the way out.

An invalid value does not crash the component and emits nothing: the panel shows #000000 and the model stays as it is. The component has no right to rewrite someone else’s data silently.

Internally the colour lives in HSLA and separately from the model, because hex is a lossy projection: black, white and any grey have no hue. Were the component to keep its state in hex alone, the hue slider would jump to 0° every time the user takes the saturation down to zero.

The channels are stored as fractions in the process. The integer HSL grid is coarser than the hex grid, and snapping to it changes the colour: of twelve typical brand values eleven would come back different — #10b981 as #10b77f. Integers are needed by the scales, and the scales get them: a slider reads the rounding of its channel, while the stored value stays exact. That is why a selected preset, a pasted hex and a colour taken with the eyedropper come back exactly the same.

Two views of the panel

view decides what sets the saturation and the lightness. The hue and the transparency are sliders in both views.

viewWhat is in the panel
sliders (the default)a ladder of channels: H, S, L and A — each a GrSlider
areaa saturation × lightness square instead of S and L

The default stayed as it was on purpose: the view changes the look of panels already standing in applications, and that decision belongs to the application rather than to the package. It is also switched globally — <GrConfigProvider :component-defaults="{ GrColorPicker: { view: 'area' } }">.

The square is voiced, not only drawn

The usual trouble with a 2D area is that it is a mouse thing: it has neither values a screen reader would read nor keys. Here inside the square there are two real input[type=range], one per axis: they are visually hidden but stay in the tab order with their names and values, and the focus is shown by the area itself through focus-within. A screen reader sees two sliders — “Saturation 90 %” and “Lightness 66 %” — rather than a nameless picture.

Hence the two Tab stops per square: there are two axes, as there are two thumbs in a GrSlider range. The keys — ../keyboard.md.

The fill of the square is exact, not approximate

The square is assembled from three layers: a saturation scale at 50 % lightness and a pair of veils, white on top and black at the bottom. This is not a device “by eye”: lightening and darkening in HSL are linear interpolation towards white and towards black, exactly what blending with alpha does. The colour under the handle matches the hex to the last digit, and that is verified by a pixel in a live browser.

The axes are saturation and lightness rather than the saturation and value familiar from HSV. Converting to HSV would give a prettier fill but would bring a second lossy projection: at zero value the HSV saturation is undefined, and black would lose it the way grey loses its hue.

The eyedropper

eyedropper adds a button beside the hex field. It is drawn only where the browser provides EyeDropper — today that is Chromium; elsewhere it is absent altogether rather than present and not working. A refusal by the user (Esc during the pick) changes nothing and reports nothing: it is not an error.

What arrives from the screen is an already blended colour, with no transparency of its own — the current one stays as it was.

The panel is non-modal

Tab out of the panel moves the focus further along the page rather than trapping it inside: nothing behind the page is blocked, and there is nothing to trap the user for. Esc closes the panel and returns the focus to the trigger — that is done by the shared layer stack through GrPopover.

The opening can be controlled from the outside: v-model:open, and the position with placement.

The presets

presets is an array of hex strings. Invalid ones are filtered out, and the selected preset gets aria-pressed. If it is empty, there is no presets block at all.

Theme tokens are not substituted into presets: --gr-primary lives as a CSS variable and is not resolved into hex at build time. The palette is assembled by the application — from its own config or from getComputedStyle.

The form

The whole form-control contract: disabled, readonly, invalid, required, ariaLabel, the update:modelValue/change/focus/blur emits, the exported focus()/blur(). The name comes from GrFormField (<label for> points at the trigger) or from ariaLabel.

name gives the current value to a native form through a hidden field — there must be no interactive elements inside the widget.

There is no clearable on purpose: a colour has no empty state. If “not set” is needed, that is undefined in the model at the level of the application, not a state of the control.

Limits
  • there is no history of recent colours — that is the state of the application;
  • there are no gradients — the component is about a single colour.