Skip to content

Instantly share code, notes, and snippets.

@roobie
Created February 3, 2026 21:31
Show Gist options
  • Select an option

  • Save roobie/87139aa7997494b7a0ff9fb3ace08291 to your computer and use it in GitHub Desktop.

Select an option

Save roobie/87139aa7997494b7a0ff9fb3ace08291 to your computer and use it in GitHub Desktop.
colors.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Live Editable Tech Palette with Randomizer</title>
<!-- Coloris -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.css"
/>
<script src="https://cdn.jsdelivr.net/gh/mdbassit/Coloris@latest/dist/coloris.min.js"></script>
<!--
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
-->
<style>
:root {
--bg: #211a3d; /* Background */
--fg: #fffeee; /* Foreground */
--accent1: #1d9ec2; /* Accent 1 */
--accent2: #b5be00; /* Accent 2 */
--text-dark: #111827;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background: var(--bg);
color: var(--fg);
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
border-bottom: 1px solid rgba(232, 241, 255, 0.14);
background: linear-gradient(90deg, #020617, #0f172a);
}
.logo {
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
font-size: 0.9rem;
color: var(--fg);
}
.nav-links {
display: flex;
gap: 1.5rem;
font-size: 0.85rem;
opacity: 0.9;
}
.nav-links a {
color: var(--fg);
text-decoration: none;
}
.nav-links a:hover {
color: var(--accent1);
}
.button {
display: inline-block;
margin-top: 1rem;
padding: 0.55rem 1.1rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
border: none;
cursor: pointer;
transition:
transform 0.08s ease,
box-shadow 0.08s ease,
background 0.12s ease;
}
.button-primary {
background: linear-gradient(135deg, var(--accent1), var(--accent2));
color: #102a3a;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.35);
}
.button-primary:hover {
transform: translateY(-1px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
}
.button-secondary {
background: transparent;
color: var(--fg);
border: 1px solid rgba(232, 241, 255, 0.55);
}
.hero {
padding: 3rem 2rem 1.5rem;
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1.3fr);
gap: 2.5rem;
}
.hero-kicker {
font-size: 0.8rem;
letter-spacing: 0.18em;
text-transform: uppercase;
opacity: 0.7;
margin-bottom: 0.75rem;
}
.hero-title {
font-size: 2.6rem;
line-height: 1.1;
margin: 0 0 1rem;
font-weight: 700;
}
.hero-subtitle {
max-width: 34rem;
font-size: 0.98rem;
opacity: 0.9;
margin: 0 0 1.5rem;
}
.gradient-text {
background: linear-gradient(120deg, var(--accent1), var(--accent2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.panel {
background: rgba(15, 23, 42, 0.9);
border-radius: 14px;
padding: 1.3rem 1.4rem 1.1rem;
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45);
border: 1px solid rgba(232, 241, 255, 0.12);
}
.panel-title {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.16em;
opacity: 0.7;
margin-bottom: 0.75rem;
}
.picker-row {
display: grid;
grid-template-columns: 0.6fr 1.2fr auto;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.7rem;
}
.picker-label {
font-size: 0.8rem;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.picker-input {
width: 100%;
padding: 0.45rem 0.6rem;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.6);
background: rgba(15, 23, 42, 0.95);
color: var(--fg);
font-size: 0.8rem;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}
.picker-random {
padding: 0.35rem 0.7rem;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.7);
background: rgba(15, 23, 42, 0.9);
color: var(--fg);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
cursor: pointer;
white-space: nowrap;
}
.picker-random:hover {
background: rgba(30, 64, 175, 0.7);
}
.swatch-grid {
max-width: 1100px;
margin: 0 auto 2.5rem;
padding: 0 2rem 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1.4rem;
}
.swatch {
border-radius: 12px;
padding: 1.1rem 1.2rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
font-size: 0.8rem;
}
.swatch h2 {
margin: 0 0 0.55rem;
font-size: 0.75rem;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.9;
}
.swatch p {
margin: 0.15rem 0;
opacity: 0.9;
}
.bg-1 {
background: var(--bg);
color: var(--fg);
border: 1px solid rgba(232, 241, 255, 0.14);
}
.bg-2 {
background: var(--fg);
color: var(--text-dark);
}
.bg-3 {
background: var(--accent1);
color: #1f2933;
}
.bg-4 {
background: var(--accent2);
color: #043b3a;
}
.chip {
display: inline-block;
margin-top: 0.4rem;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.7rem;
background: rgba(15, 23, 42, 0.68);
}
@media (max-width: 940px) {
.hero {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.app-header {
padding-inline: 1.25rem;
}
.hero {
padding-inline: 1.25rem;
}
.swatch-grid {
padding-inline: 1.25rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.picker-row {
grid-template-columns: 0.75fr 1.25fr auto;
}
}
@media (max-width: 520px) {
.hero-title {
font-size: 2rem;
}
.swatch-grid {
grid-template-columns: 1fr;
}
.picker-row {
grid-template-columns: 1fr;
}
.picker-random {
justify-self: flex-start;
}
}
#export-code pre {
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(148, 163, 184, 0.6);
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
font-size: 0.8rem;
color: var(--fg);
}
</style>
</head>
<body>
<header class="app-header">
<div class="logo">YourTech</div>
<nav class="nav-links">
<a href="#">Product</a>
<a href="#">Pricing</a>
<a href="#">Docs</a>
</nav>
<button class="button button-primary">Get Started</button>
</header>
<section class="hero">
<div>
<div class="hero-kicker">Palette Playground</div>
<h1 class="hero-title">
Design a
<span class="gradient-text">soft‑bold tech identity</span>
</h1>
<p class="hero-subtitle">
Tweak your background, foreground, and dual accents live. The hero,
buttons, and swatches update instantly to show how the palette behaves
in a real interface.
</p>
<button class="button button-primary">See It in Action</button>
<button class="button button-secondary" style="margin-left: 0.75rem">
Secondary Action
</button>
</div>
<aside class="panel">
<div class="panel-title">Adjust Colors</div>
<div class="picker-row">
<span class="picker-label">Background</span>
<input
id="bg-picker"
class="picker-input coloris"
data-coloris
value="#211a3d"
/>
<button type="button" class="picker-random" data-target="bg-picker">
Randomize
</button>
</div>
<div class="picker-row">
<span class="picker-label">Foreground</span>
<input
id="fg-picker"
class="picker-input coloris"
data-coloris
value="#fffeee"
/>
<button type="button" class="picker-random" data-target="fg-picker">
Randomize
</button>
</div>
<div class="picker-row">
<span class="picker-label">Accent&nbsp;1</span>
<input
id="accent1-picker"
class="picker-input coloris"
data-coloris
value="#1d9ec2"
/>
<button
type="button"
class="picker-random"
data-target="accent1-picker"
>
Randomize
</button>
</div>
<div class="picker-row">
<span class="picker-label">Accent&nbsp;2</span>
<input
id="accent2-picker"
class="picker-input coloris"
data-coloris
value="#b5be00"
/>
<button
type="button"
class="picker-random"
data-target="accent2-picker"
>
Randomize
</button>
</div>
<button type="button" id="export-button" class="export-button">
Export CSS
</button>
</aside>
</section>
<main class="swatch-grid">
<section class="swatch bg-1">
<h2>Background</h2>
<p><strong>Var:</strong> --bg</p>
<p><strong>Use:</strong> App shell, hero, dark sections.</p>
<span class="chip">Text & UI on background</span>
</section>
<section class="swatch bg-2">
<h2>Foreground</h2>
<p><strong>Var:</strong> --fg</p>
<p><strong>Use:</strong> Cards, light panels, primary text.</p>
<span class="chip" style="background: var(--bg); color: var(--fg)">
Foreground on dark
</span>
</section>
<section class="swatch bg-3">
<h2>Accent 1</h2>
<p><strong>Var:</strong> --accent1</p>
<p><strong>Use:</strong> Primary CTAs, key highlights.</p>
<span class="chip" style="background: var(--bg); color: var(--accent1)">
Warm accent on dark
</span>
</section>
<section class="swatch bg-4">
<h2>Accent 2</h2>
<p><strong>Var:</strong> --accent2</p>
<p><strong>Use:</strong> Secondary CTAs, charts, tags.</p>
<span class="chip" style="background: var(--bg); color: var(--accent2)">
Cool accent on dark
</span>
</section>
</main>
<div id="export-code" class="panel" style="display: none; margin-top: 2rem">
<div class="panel-title">Exported CSS</div>
<pre><code id="css-code"></code></pre>
<button
id="close-export"
class="button button-secondary"
style="margin-top: 1rem"
>
Close
</button>
</div>
<script>
// Coloris config
Coloris.setInstance(".coloris", {
theme: "large",
alpha: false,
formatToggle: true,
closeButton: true,
clearButton: false,
});
const root = document.documentElement;
const bindings = [
{ id: "bg-picker", var: "--bg" },
{ id: "fg-picker", var: "--fg" },
{ id: "accent1-picker", var: "--accent1" },
{ id: "accent2-picker", var: "--accent2" },
];
function hexToRgb(hex) {
let h = hex.trim();
if (h.startsWith("#")) h = h.slice(1);
if (h.length === 3) {
h = h
.split("")
.map((c) => c + c)
.join("");
}
if (h.length !== 6) return null;
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
return { r, g, b };
}
function rgbToHex(r, g, b) {
const toHex = (v) => v.toString(16).padStart(2, "0");
return "#" + toHex(r) + toHex(g) + toHex(b);
}
// Randomize within ±10% of the current channel value, clamped to [0,255]
function randomNearHex(hex) {
const rgb = hexToRgb(hex);
if (!rgb) return hex;
const jitterChannel = (val) => {
const delta = Math.max(1, Math.round(val * 0.1)); // 10% of channel
const min = Math.max(0, val - delta);
const max = Math.min(255, val + delta);
return Math.round(min + Math.random() * (max - min));
};
const r = jitterChannel(rgb.r);
const g = jitterChannel(rgb.g);
const b = jitterChannel(rgb.b);
return rgbToHex(r, g, b);
}
// Bind inputs to CSS variables
bindings.forEach(({ id, var: cssVar }) => {
const input = document.getElementById(id);
if (!input) return;
const updateVar = () => {
const value = input.value.trim();
if (!value) return;
root.style.setProperty(cssVar, value);
};
input.addEventListener("input", updateVar);
input.addEventListener("change", updateVar);
});
// Randomize buttons
document.querySelectorAll(".picker-random").forEach((btn) => {
const targetId = btn.getAttribute("data-target");
const binding = bindings.find((b) => b.id === targetId);
if (!binding) return;
const input = document.getElementById(binding.id);
btn.addEventListener("click", () => {
const current = input.value || "#000000";
const next = randomNearHex(current);
input.value = next;
root.style.setProperty(binding.var, next);
// Trigger Coloris preview update if open
const ev = new Event("input", { bubbles: true });
input.dispatchEvent(ev);
});
});
// Export CSS functionality
const exportButton = document.getElementById("export-button");
const exportCodeDiv = document.getElementById("export-code");
const cssCodeEl = document.getElementById("css-code");
const closeExportBtn = document.getElementById("close-export");
exportButton.addEventListener("click", () => {
// Get current values from CSS variables
const bg = getComputedStyle(root).getPropertyValue("--bg").trim();
const fg = getComputedStyle(root).getPropertyValue("--fg").trim();
const accent1 = getComputedStyle(root)
.getPropertyValue("--accent1")
.trim();
const accent2 = getComputedStyle(root)
.getPropertyValue("--accent2")
.trim();
// Generate CSS string
const css = `:root {
--bg: ${bg};
--fg: ${fg};
--accent1: ${accent1};
--accent2: ${accent2};
}`;
// Populate and show the code block
cssCodeEl.textContent = css;
exportCodeDiv.style.display = "block";
// Optional: Scroll to the code block
exportCodeDiv.scrollIntoView({ behavior: "smooth" });
});
// Close button
closeExportBtn.addEventListener("click", () => {
exportCodeDiv.style.display = "none";
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment