Color Gradient
The color box's picker surfaces as an always-visible form editor — Kendo's ColorGradient: a saturation/brightness surface, hue and optional alpha sliders, hex + R/G/B(/A) inputs and an optional WCAG contrast readout against a background you choose. No APG color-picker pattern exists, so it is a labelled role="group" of APG sliders (the surface is a 2-axis role="slider" with mandatory aria-valuetext) and native inputs.
Works standalone via [(value)], with Signal Forms via [formField], and with reactive/template forms via formControl/ngModel. Need it in a drop-down? That is the color box.
Getting started
Surface, hue slider and channel inputs, each its own Tab stop. Dragging commits live and flushes on release; Escape mid-drag restores the start color. The hue survives a pass through gray — the working color keeps its own HSV.
Value: #3aa0ff
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeColorGradient } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeColorGradient],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The color box's picker surfaces, inline and always visible: a 2-axis
role="slider" surface (aria-valuetext names both axes), the APG hue
slider and hex + R/G/B inputs — each its own Tab stop inside one
labelled role="group". Dragging commits live and flushes on release;
Escape mid-drag restores the start color. -->
<oge-color-gradient label="Brand color" [(value)]="brand" />
`,
})
export class Demo {
protected readonly brand = signal<string | null>('#3aa0ff');
}Alpha and formats
editAlphaChannel adds the alpha slider and percent input; format: 'hex' | 'rgb' | 'rgba' | 'hsl' fixes the committed string, widening to carry alpha when the color is translucent. showInputs: false keeps only the surface and sliders.
Overlay: rgba(58, 160, 255, 0.5) — Compact: hsl(160, 84%, 39%)
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeColorGradient } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeColorGradient],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- editAlphaChannel adds the alpha slider + percent input. format fixes
the committed shape; a translucent color widens it to carry alpha.
showInputs=false keeps just the surface and sliders. -->
<oge-color-gradient
label="Overlay"
format="rgba"
[editAlphaChannel]="true"
[(value)]="overlay"
/>
<oge-color-gradient
label="Compact (hsl)"
format="hsl"
[showInputs]="false"
[(value)]="accent"
/>
`,
})
export class Demo {
protected readonly overlay = signal<string | null>('rgba(58, 160, 255, 0.5)');
protected readonly accent = signal<string | null>('hsl(160, 84%, 39%)');
}Contrast checker
showContrast renders the WCAG readout — the ratio of the current color against contrastBackground (translucent colors are composited over it first) with AA (4.5:1) and AAA (7:1) verdicts. A failing 4.499 never displays as a pass. The math is exported as contrastRatio() / contrastLevels().
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeColorGradient, contrastRatio, contrastLevels } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeColorGradient],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- showContrast renders the WCAG readout — the ratio of the current
color against contrastBackground (translucent colors are composited over
it first) and AA (4.5:1) / AAA (7:1) verdicts. The same math is exported:
contrastRatio() + contrastLevels(). -->
<oge-color-gradient
label="Text color"
[showContrast]="true"
contrastBackground="#ffffff"
[(value)]="text"
/>
`,
})
export class Demo {
protected readonly text = signal<string | null>('#767676');
/** The same verdict in code — e.g. to block a save. */
protected readonly passesAa = (fg: { r: number; g: number; b: number; a: number }) =>
contrastLevels(contrastRatio(fg, { r: 255, g: 255, b: 255, a: 1 })).aa;
}Inside a form
A FormValueControl and a ControlValueAccessor like every editor in the family: [formField], formControl and ngModel all bind it, and touched/dirty/disabled follow the bound field.
Model: #7c3aed
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeColorGradient } from '@oge-ui/inputs';
import { FormField, form, required } from '@angular/forms/signals';
@Component({
selector: 'demo-root',
imports: [OgeColorGradient, FormField],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A FormValueControl and a ControlValueAccessor like every editor:
[formField], formControl and ngModel all bind it. -->
<oge-color-gradient label="Theme color" [formField]="f.theme" />
`,
})
export class Demo {
protected readonly model = signal<{ theme: string | null }>({ theme: '#7c3aed' });
protected readonly f = form(this.model, (p) => {
required(p.theme);
});
}