Skip to content

Instantly share code, notes, and snippets.

@mike-at-redspace
Created July 15, 2025 10:56
Show Gist options
  • Save mike-at-redspace/09155e6c1782d652a7d3f385bcabf283 to your computer and use it in GitHub Desktop.
Save mike-at-redspace/09155e6c1782d652a7d3f385bcabf283 to your computer and use it in GitHub Desktop.
Open Props - oklch colors v2 config
<header>
<section>
<p>Hue</p>
<div class="swatch max"><b><code>var(--color-max)</code></b></div>
</section>
<section>
<div class="title-with-control">
<p>Options</p>
<button id="shuffle" title="You can also press spacebar!">
<svg viewBox="0 0 24 24" width="17" height="24">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
</div>
<div class="controls">
<div>
<label for="paletteHue">Hue</label>
<input id="paletteHue" type="range" min="0" max="360" value="358">
<output id="paletteHueValue">0</output>
</div>
<div>
<label for="paletteHueRotate">Hue Rotation</label>
<input id="paletteHueRotate" type="range" min="-30" max="30" value="-5">
<output id="paletteHueRotateValue">0</output>
</div>
<div>
<label for="paletteChroma">Chroma</label>
<input id="paletteChroma" type="range" min="0" max="2" step="0.01" value="0.9">
<output id="paletteChromaValue">1</output>
</div>
</div>
</section>
<section>
<p>Page</p>
<fieldset id="bg">
<legend>Page Background</legend>
<label>
<input type="radio" name="page-background" value="var(--color-16)" checked>
Palette Darkest
</label>
<label>
<input type="radio" name="page-background" value="var(--color-1)">
Palette Lightest
</label>
<label>
<input type="radio" name="page-background" value="white">
White
</label>
<label>
<input type="radio" name="page-background" value="black">
Black
</label>
</fieldset>
</section>
<section>
<p>Presets</p>
<div class="preset-group">
<button id="openprops">Open Props</button>
</div>
</section>
</header>
<article class="swatchset" id="swatchset">
<div class="swatch"><b><code>var(--color-1)</code></b></div>
<div class="swatch"><b><code>var(--color-2)</code></b></div>
<div class="swatch"><b><code>var(--color-3)</code></b></div>
<div class="swatch"><b><code>var(--color-4)</code></b></div>
<div class="swatch"><b><code>var(--color-5)</code></b></div>
<div class="swatch"><b><code>var(--color-6)</code></b></div>
<div class="swatch"><b><code>var(--color-7)</code></b></div>
<div class="swatch"><b><code>var(--color-8)</code></b></div>
<div class="swatch"><b><code>var(--color-9)</code></b></div>
<div class="swatch"><b><code>var(--color-10)</code></b></div>
<div class="swatch"><b><code>var(--color-11)</code></b></div>
<div class="swatch"><b><code>var(--color-12)</code></b></div>
<div class="swatch"><b><code>var(--color-13)</code></b></div>
<div class="swatch"><b><code>var(--color-14)</code></b></div>
<div class="swatch"><b><code>var(--color-15)</code></b></div>
<div class="swatch"><b><code>var(--color-16)</code></b></div>
</article>
<output class="code-output">
<pre id="paletteout">:root {
--palette-hue: 358;
--palette-hue-rotate-by: -5;
--palette-chroma: .9;
}</pre>
</output>
<!-- <section>
<dl>
<dt><b>17 Colors</b></dt>
<dd>8 dark</dd>
<dd>8 light</dd>
<dd>1 max</dd>
</dl>
</section> -->
<section class="normalize">
<div class="title">
<b>Open Props</b><sup>v2</sup>
<p>Example palette usage :: normalize.css theme</p>
</div>
<p><b>Light</b></p>
<p><b>Dark</b></p>
<div class="light">
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box"><p>Surface</p> <span>1</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box"><p>Surface</p> <span>2</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box"><p>Surface</p> <span>3</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box well"><p>Well</p> <span>1</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box well"><p>Well</p> <span>2</span></div>
</div>
</div>
<div class="dark">
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box"><p>Surface</p> <span>1</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box"><p>Surface</p> <span>2</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box"><p>Surface</p> <span>3</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box well"><p>Well</p> <span>1</span></div>
</div>
<div style="display: flex;">
<div class="contrast"><p></p><p></p></div>
<div class="box well"><p>Well</p> <span>2</span></div>
</div>
</div>
</section>
<footer>
<p>Press <code>spacebar</code> to shuffle</p>
<!-- <small>Open Props v2 Colors PreBeta</small> -->
</footer>
import Color from 'https://colorjs.io/dist/color.js'
// page background
bg.oninput = e => {
const val = e.target.value
document.body.style.background = val
document.firstElementChild.className = val === 'white' || val === 'var(--color-1)' || e.target.hasAttribute('light')
? 'light'
: 'dark'
}
openprops.onclick = e => {
document.body.style.background = 'var(--surface-document)'
bg.querySelector('input[type="radio"]:checked').checked = false
palette.hue = 270
palette.hueRotateBy = -5
palette.chroma = .1
updateSheet()
updateInputs()
}
shuffle.onclick = randomizeColors
// good use case for observable .on with a filter
window.onkeypress = e => {
if (e.code === 'Space') {
e.preventDefault()
randomizeColors()
}
}
function randomizeColors() {
shuffle.animate([
{ transform: 'rotate(1turn)' }
], {
duration: 1000,
easing: CSS.supports('animation-timing-function: linear(0, 1)')
? `linear(
0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%, 1.017,
1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%,
1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%,
0.997 69.8%, 1.003 76.9%, 1.004 83.8%, 1
)`
: `ease`,
})
if (!bg.querySelector('input[type="radio"]:checked')) {
bg.querySelector('input[type="radio"]').checked = true
document.body.style.background = 'var(--color-16)'
}
palette.hue = Math.round(Math.random() * 360)
palette.hueRotateBy = Math.round(Math.random() * 6 * (Math.random() > .5 ? 1 : -1))
palette.chroma = Math.random()
updateInputs()
}
function updateInputs() {
paletteHue.value = palette.hue
paletteHueRotate.value = palette.hueRotateBy
paletteChroma.value = palette.chroma
paletteHue.dispatchEvent(new Event('input'))
paletteHueRotate.dispatchEvent(new Event('input'))
paletteChroma.dispatchEvent(new Event('input'))
determineContrast()
}
// palette options
const palette = {
hue: 358,
hueRotateBy: -5,
chroma: .9,
}
paletteHue.oninput = e => {
paletteHueValue.textContent = e.target.value
palette.hue = e.target.value
updateSheet()
}
paletteHueRotate.oninput = e => {
paletteHueRotateValue.textContent = e.target.value
palette.hueRotateBy = e.target.value
updateSheet()
}
paletteChroma.oninput = e => {
paletteChromaValue.textContent = e.target.value
palette.chroma = e.target.value
updateSheet()
}
function updateSheet() {
const styles = `:root {
--palette-hue: ${palette.hue};
--palette-hue-rotate-by: ${palette.hueRotateBy};
--palette-chroma: ${palette.chroma};
}`
paletteSheet.replaceSync(styles, 0)
paletteout.innerText = styles
}
const paletteSheet = new CSSStyleSheet()
document.adoptedStyleSheets = [...document.adoptedStyleSheets, paletteSheet]
updateSheet()
paletteHue.dispatchEvent(new Event('input'))
paletteHueRotate.dispatchEvent(new Event('input'))
paletteChroma.dispatchEvent(new Event('input'))
determineContrast()
function determineContrast() {
document.querySelectorAll('.box').forEach(node => {
const bg = new Color(node.computedStyleMap().get('background-color').toString())
const [p, span] = node.children
const c2 = new Color(p.computedStyleMap().get('color').toString())
const c3 = new Color(span.computedStyleMap().get('color').toString())
const [primary,secondary] = node.previousElementSibling.children
primary.textContent = 'AA ' + bg.contrast(c2, 'wcag21').toFixed(1)
secondary.textContent = 'AA ' + bg.contrast(c3, 'wcag21').toFixed(1)
})
}
/* Open Props v2 color palette import (beta) */
@import "https://unpkg.com/[email protected]/palette.css";
.swatchset {
inline-size: clamp(50vw, 75vw, 1024px);
display: grid;
grid-auto-flow: column;
grid-auto-columns: 5vw;
& > .swatch:nth-of-type(1) { background: var(--color-1) }
& > .swatch:nth-of-type(2) { background: var(--color-2) }
& > .swatch:nth-of-type(3) { background: var(--color-3) }
& > .swatch:nth-of-type(4) { background: var(--color-4) }
& > .swatch:nth-of-type(5) { background: var(--color-5) }
& > .swatch:nth-of-type(6) { background: var(--color-6) }
& > .swatch:nth-of-type(7) { background: var(--color-7) }
& > .swatch:nth-of-type(8) { background: var(--color-8) }
& > .swatch:nth-of-type(9) { background: var(--color-9) }
& > .swatch:nth-of-type(10) { background: var(--color-10) }
& > .swatch:nth-of-type(11) { background: var(--color-11) }
& > .swatch:nth-of-type(12) { background: var(--color-12) }
& > .swatch:nth-of-type(13) { background: var(--color-13) }
& > .swatch:nth-of-type(14) { background: var(--color-14) }
& > .swatch:nth-of-type(15) { background: var(--color-15) }
& > .swatch:nth-of-type(16) { background: var(--color-16) }
}
/* Open Props Normalize.css */
.light {
--surface-1: white;
--surface-2: var(--color-1);
--surface-3: var(--color-3);
--surface-document: var(--color-4);
--well-1: var(--color-5);
--well-2: var(--color-6);
--text-1: var(--color-16);
--text-2: var(--color-12);
color-scheme: light;
}
.dark {
--surface-1: var(--color-11);
--surface-2: var(--color-12);
--surface-3: var(--color-13);
--surface-document: var(--color-14);
--well-1: var(--color-15);
--well-2: var(--color-16);
--text-1: var(--color-1);
--text-2: var(--color-5);
color-scheme: dark;
}
.swatch {
inline-size: 5vw;
aspect-ratio: 1;
display: grid;
place-content: start center;
white-space: nowrap;
cursor: pointer;
border-radius: 0px;
transition: border-radius .2s ease;
& > b {
transition: opacity .2s ease, transform .2s ease;
opacity: 0;
transform: scale(.8);
}
&:hover {
border-radius: 50%;
& > b {
opacity: 1;
transform: translateY(-125%);
}
}
&.max {
background: oklch(70% 100% var(--palette-hue));
}
}
.controls {
display: grid;
justify-content: start;
gap: 1.25vmin;
grid-auto-rows: 1lh;
& > div {
display: flex;
align-items: center;
gap: .5ch;
& input {
flex: 1;
}
& output {
text-align: right;
inline-size: 3ch;
color: var(--text-2);
}
}
}
header {
display: flex;
gap: 5vw;
@media (width > 1024px) {
gap: 10vw;
}
}
html {
block-size: 100%;
background: var(--color-16);
color: var(--text-1);
accent-color: var(--color-max);
}
:focus {
outline-color: oklch(from var(--color-max) l c h / 75%);
}
body {
min-block-size: 100%;
font-family: system-ui, sans-serif;
display: grid;
place-content: center start;
margin: 0;
padding: 10vmin;
gap: 5vmin;
}
header > section {
display: grid;
gap: 2.5vmin;
align-content: start;
p {
font-weight: bold;
}
> p, & label {
white-space: nowrap;
color: var(--text-1);
}
}
.normalize {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem 5rem;
max-inline-size: 70ch;
> .title {
grid-column: span 2;
}
> :is(.light,.dark) {
display: grid;
grid-auto-rows: 10ch;
gap: 0 1rem;
}
:is(.light, .dark) > div {
&:nth-of-type(1) > .box { background: var(--surface-1) }
&:nth-of-type(2) > .box { background: var(--surface-2) }
&:nth-of-type(3) > .box { background: var(--surface-3) }
&:nth-of-type(4) > .box { background: var(--well-1) }
&:nth-of-type(5) > .box { background: var(--well-2) }
}
}
.box {
display: grid;
place-items: center;
place-content: center;
flex: 1;
& > span {
color: var(--text-2);
font-size: 3rem;
}
& > p {
color: var(--text-1);
margin: 0;
}
}
.contrast {
display: grid;
place-content: center start;
gap: 1rem;
min-inline-size: 8ch;
}
fieldset {
border: none;
margin: 0;
padding: 0;
display: grid;
gap: 1.25vmin;
& > legend {
height: 0;
overflow: hidden;
clip: 0;
}
}
input[type="range"] {
&::-webkit-slider-runnable-track {
height: 2px;
/* background: var(--text-2); */
}
&::-webkit-slider-thumb {
margin-top: -7px;
cursor: pointer;
}
}
input[type="radio"] {
margin-inline-start: 0;
}
p {
margin: 0;
}
.title-with-control {
display: flex;
justify-content: space-between;
align-items: center;
> button {
padding: 0;
margin: 0;
background: none;
border: none;
border-radius: 1e3px;
aspect-ratio: 1;
display: inline-flex;
place-items: center;
cursor: pointer;
transform-origin: center;
outline-offset: 3px;
> svg {
block-size: 1lh;
stroke-width: 2px;
fill: none;
stroke: var(--text-2);
}
&:hover > svg {
stroke: var(--text-1);
}
}
}
.code-output {
background: var(--well-1);
padding-inline: 1rem;
border-radius: 20px;
max-inline-size: max-content;
}
.preset-group {
display: flex;
align-items: center;
gap: 1ch;
}
dl {
display: inline-grid;
gap: .5ch;
> dd {
grid-column: 2;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment