Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <input type="checkbox" id="toggle" hidden />
  <label for="toggle" data-app-elm="toggle">
    <svg viewBox="0 0 24 24">
      <circle cx="14" cy="6" r="2" />
      <line x1="4" y1="6" x2="12" y2="6" />
      <line x1="16" y1="6" x2="20" y2="6" />
      <circle cx="8" cy="12" r="2" />
      <line x1="4" y1="12" x2="6" y2="12" />
      <line x1="10" y1="12" x2="20" y2="12" />
      <circle cx="17" cy="18" r="2" />
      <line x1="4" y1="18" x2="15" y2="18" />
      <line x1="19" y1="18" x2="20" y2="18" />
    </svg>
    <svg viewBox="0 0 24 24">
      <line x1="18" y1="6" x2="6" y2="18" />
      <line x1="6" y1="6" x2="18" y2="18" />
    </svg>
  </label>

  <svg data-app-elm="svg" id="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000" style="background-color:hsl(248, 50%, 25%)">
    <g id="g"></g>
  </svg>

  <main>
    <form id="app" data-app="dotring">
      <details open>
        <summary>dots & rings</summary>
        <div>
          <label>
            number of rings
            <input type="range" id="numRings" min="1" max="30" data-random />
          </label>
          <label>
            dot size
            <input type="range" id="dotSize" min="1" max="30" data-random />
          </label>
          <label>
            dots per ring
            <input type="range" id="dotsPerRing" min="1" max="30" data-random />
          </label>
          <label>
            spread
            <input type="range" id="spread" min="1" max="50" data-random />
          </label>
          <label data-app-elm="checkbox">
            <input type="checkbox" id="randomRadius" data-random />
            <span>random radius</span>
          </label>
          <label data-app-elm="checkbox">
            <input type="checkbox" id="randomDotSize" data-random />
            <span>random dot-size</span>
          </label>
        </div>
      </details>

      <details>
        <summary>color ranges</summary>
        <div>
          <label>
            hue min
            <input type="range" id="hueMin" min="0" max="360" data-random />
          </label>
          <label>
            hue max
            <input type="range" id="hueMax" min="0" max="360" data-random />
          </label>
          <label>
            saturation min
            <input type="range" id="satMin" min="0" max="100" data-suffix="%" data-random />
          </label>
          <label>
            saturation max
            <input type="range" id="satMax" min="0" max="100" data-suffix="%" data-random />
          </label>
          <label>
            lightness min
            <input type="range" id="lightMin" min="0" max="100" data-suffix="%" data-random />
          </label>
          <label>
            lightness max
            <input type="range" id="lightMax" min="0" max="100" data-suffix="%" data-random />
          </label>
          <label>
            custom color-array:
            <input type="text" id="colorArray" placeholder="#FFF|white|hsl(0,0%,100%)" />
          </label>
        </div>
      </details>

      <details>
        <summary>background / canvas</summary>
        <div>
          <label>
            hue
            <input type="range" id="hueBg" min="0" max="360" data-random data-attr />
          </label>
          <label>
            saturation
            <input type="range" id="satBg" min="0" max="100" data-suffix="%" data-random data-attr />
          </label>
          <label>
            lightness
            <input type="range" id="lightBg" min="0" max="100" data-suffix="%" data-random data-attr />
          </label>
          <label>
            canvas y:x ratio
            <input type="range" id="canvasRatio" min="25" max="150" value="100" />
          </label>
          <label>
            rotate
            <input type="range" min="0" max="360" value="0" id="rotate" data-attr />
          </label>
          <label>
            scale-x
            <input type="range" min="0.01" max="3" step="0.01" value="1" id="scaleX" data-attr />
          </label>
          <label>
            scale-y
            <input type="range" min="0.01" max="3" step="0.01" value="1" id="scaleY" data-attr />
          </label>
          <label>
            translate-x
            <input type="range" min="-1000" max="1000" value="0" id="translateX" data-attr />
          </label>
          <label>
            translate-y
            <input type="range" min="-1000" max="1000" value="0" id="translateY" data-attr />
          </label>
        </div>
      </details>

      <details>
        <summary>export</summary>
        <div>
          <fieldset>
            <legend>file format</legend>
            <label data-app-elm="radio">
              <input type="radio" name="fileFormat" value="svg" data-ignore>
              <span>svg</span>
            </label>
            <label data-app-elm="radio">
              <input type="radio" name="fileFormat" value="" data-ignore checked>
              <span>png</span>
            </label>
            <label data-app-elm="radio">
              <input type="radio" name="fileFormat" value="jpg" data-ignore>
              <span>jpg</span>
            </label>
            <label data-app-elm="radio">
              <input type="radio" name="fileFormat" value="webp" data-ignore>
              <span>webp</span>
            </label>
          </fieldset>
          <button type="button" onclick="saveFile(svg, app.elements.fileFormat.value)">Save To Image</button>
        </div>
      </details>
      <br />
      <button type="button" onclick="randomPreset()">Random!</button>
    </form>
  </main>
              
            
!

CSS

              
                * { 
  box-sizing: border-box;
	margin: unset;
}

html {
  block-size: 100%;
  inline-size: 100%;
}

body {
  --app-w: 20rem;
  --bgc: hsla(200, 30%, 85%, 0.95);
  --bg-w: 100%;
  background-color: var(--bgc);
  min-block-size: 100%;
  min-inline-size: 100%;
}

[data-app] {
  --accent: hsl(200, 50%, 50%);
	--accent-bg: hsl(200, 30%, 55%);
  --border: hsl(200, 30%, 30%);
  --bdrs: 0.15rem;
  --gap: 1rem;
  --rng-h: 2rem;

  background: var(--bgc);
  border-inline-start: 2px solid var(--border);
  bottom: 0;
  font-family: ui-monospace, monospace;
  height: 100vh;
  left: var(--app-l, 100%);
  overflow-y: auto;
  padding-inline: var(--gap);
  position: fixed;
  right: 0;
  top: 0;
  transition: left 0.5s cubic-bezier(.35, .92, 1, 1);
  width: var(--app-w);
  z-index: 1;
}

[data-app] button {
  background-color: var(--accent-bg);
  border: 1px solid var(--border);
  box-shadow: 5px 5px 0 0 var(--border);
  font-family: inherit;
  padding: calc(var(--gap) / 2) var(--gap);
}

button[data-app-elm="preset"] {
  background-color: hsl(200, 30%, 20%);
  border: 0;
  border-radius: 0.75rem;
  box-shadow: none;
  color: hsl(200, 30%, 95%);
  font-family: inherit;
  font-size: x-small;
  margin: 0 0.25rem .5rem 0;
  padding: 0.25rem 0.75rem
}

[data-app-elm="checkbox"],
[data-app-elm="radio"] {
  display: block;
  font-family: ui-monospace, monospace;
  margin-block-end: var(--gap);
}

[data-app-elm="checkbox"] span,
[data-app-elm="radio"] span {
  align-items: center;
  display: flex;
  position: relative;
}

[data-app-elm="checkbox"] span::before,
[data-app-elm="radio"] span::before {
  background-color: var(--accent-bg);
  border: 2px solid var(--border);
  border-radius:50%;
  content: '';
  display: inline-block;
  height: 1.5rem;
  margin-inline-end: 0.5rem;
  width: 1.5rem;
}

[data-app-elm="checkbox"] input,
[data-app-elm="radio"] input {
  clip: rect(0 0 0 0); 
  clip-path: inset(50%);
  height: 1px;
  left: 0;
  overflow: hidden;
  position: absolute;
  white-space: nowrap; 
  width: 1px;
}

[data-app-elm] input:checked + span::before {
  background-color: var(--border);
  box-shadow: inset 0 0 0 6px var(--accent-bg);
}

[data-app-elm="checkbox"] span::before {
  border-radius: 0.25rem;
}

[data-app-elm="svg"] {
  transition: width 0.5s cubic-bezier(.35, .92, 1, 1);
  width: var(--bg-w);
}

[data-app-elm="toggle"] {
  background-color: hsl(200, 15%, 15%);
  border-color: hsl(200, 15%, 95%);
  border-style: solid;
  border-width: 0 1px 1px 0;
  height: 44px;
  position: absolute;
  width: 44px;
}

[data-app-elm="toggle"] svg {
  fill: none;
  position: absolute;
  stroke: hsl(200, 15%, 95%);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1;
  transition: opacity 1s cubic-bezier(.35, .92, 1, 1);
}

[data-app-elm="toggle"] svg:last-of-type {
  opacity: 0;
}

[data-app] fieldset {
  border: 1px solid var(--accent-bg);
  border-radius: var(--bdrs);
  margin-block-end: var(--gap);
  padding: var(--gap);
}

[data-app] label {
  display: block;
  position: relative;
}

[data-app] label::after {
  content: attr(data-value);
  display: inline-block;
  font-size: x-small;
  position: absolute;
  right: 0;
  top: 0.5em;
}

[data-app] summary {
	border-bottom: 2px dashed var(--accent-bg);
	cursor: pointer;
	padding-block: var(--gap);
	user-select: none;
}

[data-app] summary + div {
	border-bottom: 2px dashed var(--accent-bg);
	padding-block: var(--gap);
}

[data-app] [type="range"] {
  --rng-bdrs: .5rem;
  --rng-bgi: linear-gradient(to right, var(--accent-bg), var(--accent-bg));
  --rng-h: 0.5rem;
  --rng-m: var(--gap) 0;
  --rng-thumb-bdrs: 50%;
  --rng-thumb-bgc: var(--accent);
  --rng-thumb-bxsh: inset 0 0 0 0.125rem var(--border);
  --rng-thumb-bxsh--focus: inset 0 0 0 0.125rem var(--border), 0 0 0 0.125rem  rgba(255, 255, 255, 0.8);
  --rng-thumb-h: 2rem;
  --rng-thumb-w: 2rem;

  background-image: var(--rng-bgi, inherit);
  border-radius: var(--rng-bdrs, 0);
  font-family: inherit;
  height: var(--rng-h);
  margin: var(--rng-m, 0);
  outline: 0.25rem solid transparent;
  position: relative;
  touch-action: none;
  width: 100%;
}

[data-app] [type="range"][max="360"] {
	--s: 60%;
  --rng-bgi: linear-gradient(to right, 
  hsla(0, var(--s), 50%, 0.8), 
  hsla(30, var(--s), 50%, 0.8), 
  hsla(60, var(--s), 50%, 0.8), 
  hsla(90, var(--s), 50%, 0.8), 
  hsla(120, var(--s), 50%, 0.8), 
  hsla(150, var(--s), 50%, 0.8), 
  hsla(180, var(--s), 50%, 0.8), 
  hsla(210, var(--s), 50%, 0.8), 
  hsla(240, var(--s), 50%, 0.8), 
  hsla(270, var(--s), 50%, 0.8), 
  hsla(300, var(--s), 50%, 0.8), 
  hsla(330, var(--s), 50%, 0.8),
  hsla(360, var(--s), 50%, 0.8)
  );
}

[data-app] [type="range"]::-moz-range-thumb {
  background-color: var(--rng-thumb-bgc);
  border-radius: var(--rng-thumb-bdrs);
  box-shadow: var(--rng-thumb-bxsh);
  color: #000;
  cursor: ew-resize;
  height: var(--rng-thumb-h);  
  margin-top: calc(0px - ((var(--rng-thumb-h) - var(--rng-h)) / 2));
  position: relative;
  width: var(--rng-thumb-w);
}

[data-app] [type="range"]::-webkit-slider-thumb {
  background-color: var(--rng-thumb-bgc);
  border-radius: var(--rng-thumb-bdrs);
  box-shadow: var(--rng-thumb-bxsh);
  cursor: ew-resize;
  height: var(--rng-thumb-h);  
  margin-top: calc(0px - ((var(--rng-thumb-h) - var(--rng-h)) / 2));
  position: relative;
  width: var(--rng-thumb-w);
}

[data-app] [type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: var(--rng-thumb-bxsh--focus);
}

[data-app] [type="range"]::-moz-range-track {
  background: transparent;
  background-size: 100%;
  height: var(--rng-h);
}

[data-app] [type="range"]::-webkit-slider-runnable-track {
  background: transparent;
  background-size: 100%;
  height: var(--rng-h);
}

[data-app] [type="range"],
[data-app] [type="range"]::-webkit-slider-runnable-track,
[data-app] [type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
}

[data-app] [type="text"] {
  border: 1px solid var(--border);
  border-radius: var(--bdrs);
  font-family: inherit;
  margin-block-end: var(--gap);
  margin-block-start: calc(var(--gap) / 4);
  padding: calc(var(--gap) / 2);
  width: 100%;
}

/* STATE */
#toggle:checked ~ [data-app] { --app-l: 25vw; }
#toggle:checked + [data-app-elm="toggle"] svg:first-of-type { opacity: 0; }
#toggle:checked + [data-app-elm="toggle"] svg:last-of-type { opacity: 1; }
#toggle:checked ~ [data-app-elm="svg"] {
  --bg-w: calc(100% - var(--app-w));
}
#toggle:checked ~ main [data-app] {
  --app-l: calc(100% - var(--app-w));
}
              
            
!

JS

              
                const apps = {
	dotring: () => {
		/* Returns an array of points (`number`) placed on a circle with `radius` */
		const coords = (number, arr = []) => {
			const frags = 360 / number;
			for (let i = 0; i <= number; i++) {
					arr.push((frags / 180) * i * Math.PI);
			}
			return arr;
		}
		const colors = colorArray.value.split('|');
		const useColorArray = colors.length > 1;

		const fill = () => useColorArray ? colors[R(colors.length)] : `hsl(${R(hueMax.valueAsNumber, hueMin.valueAsNumber)}, ${R(satMax.valueAsNumber, satMin.valueAsNumber)}%, ${R(lightMax.valueAsNumber, lightMin.valueAsNumber)}%)`;
		let s = `<circle cx="500" cy="500" r="${dotSize.valueAsNumber}" fill="${fill()}" />`;

		for (let i = 1; i <= numRings.valueAsNumber; i++ ) {
			const r = randomRadius.checked ? R(500,1) : spread.valueAsNumber * i;
			const theta = coords(dotsPerRing.valueAsNumber * i);
			for (let j = 0; j < theta.length; j++) {
				const x = 500 - Math.round(r * (Math.cos(theta[j])));
				const y = 500 - Math.round(r * (Math.sin(theta[j])));
				s+= `<circle cx="${x}" cy="${y}" r="${randomDotSize.checked ? R(35,2) : dotSize.valueAsNumber}" fill="${fill()}" />`
			}
		}
		g.innerHTML = s;
	}
}

function render(input) {
	if (input?.type === 'range') {
		input.parentNode.dataset.value = `${input.valueAsNumber}${input.dataset.suffix||''}`;
		if (input.name) document.body.style.setProperty(input.name, `${input.valueAsNumber}${input.dataset.suffix||''}`)
	}
	if (input?.hasAttribute('data-ignore')) return;
	if (input?.hasAttribute('data-attr')) {
		setBG();
		g.setAttribute('transform', `rotate(${rotate.valueAsNumber}, 500, ${canvasRatio.valueAsNumber * 5}) scale(${scaleX.valueAsNumber}, ${scaleY.valueAsNumber}) translate(${translateX.valueAsNumber}, ${translateY.valueAsNumber})`);
		return;
	}
	svg.setAttribute('viewBox', `0 0 1000 ${canvasRatio.valueAsNumber * 10}`);
	apps[app.dataset.app]();
}

/**
 * @function loadPreset
 * @description Loads a preset, renders preview
 * @param {Object} preset
*/
function loadPreset(preset) {
	Object.entries(preset).forEach(entry => {
		const [key, value] = [...entry];
		if (app.elements[key]?.type === 'checkbox') { 
			app.elements[key].checked = value === 1;
		}
		else {
			app.elements[key].value = value;
			app.elements[key].parentNode.dataset.value = value;
		}
	});
	render();
}

/**
 * @function R
 * @description returns a random number between max and min
 * @param {Number} max
 * @param {Number} [min]
 * @param {Boolean} [f]
*/
function R(max, min = 0, f = true) {
	return f ? Math.floor(Math.random() * (max - min) + min) : Math.random() * max;
};

/**
 * @function randomPreset
 * @description Creates a random preset
*/
function randomPreset() {
	app.reset();
	[...app.elements].forEach(input => {
		if (input.hasAttribute('data-random')) {
			if (input.type === 'checkbox') {
				input.checked = R(10, 0) > 5;
			}
			if (input.type === 'range') {
				input.value = R(input.max-0, input.min-0);
				input.parentNode.dataset.value = `${input.value}${input.dataset.suffix||''}`;
			}
		}
	});
	setBG();
	render();
}

/**
 * @function saveFile
 * @description Exports canvas to either svg, png, jpg or webp
 * @param {Node} svg
 * @param {String} [ext]
*/
function saveFile(svg, ext = 'png') {
	const download = (href, name) => {
		const L = document.createElement('a');
		L.download = name;
		L.style.opacity = "0";
		document.body.append(L);
		L.href = href;
		L.click();
		L.remove()
	}
	const {width, height} = svg.getBBox(); 
	let clone = svg.outerHTML;
	const blob = new Blob([clone],{type:'image/svg+xml;charset=utf-8'});
	const format = { png: '', jpg: 'image/jpg', webp: 'image/webp' };
	if (ext === 'svg') {
		download(URL.createObjectURL(blob), 'image.svg');
		return;
	}
	const img = new Image();
	img.onload = () => {
		const C = document.createElement('canvas');
		C.width = width;
		C.height = height;
		const X = C.getContext('2d');
		X.drawImage(img, 0, 0, width, height);
		download(C.toDataURL(format[ext], 1), `image.${ext}`)
	};
	img.src = URL.createObjectURL(blob);
}

/**
 * @function setBG
 * @description Sets canvas-background
*/
function setBG() {
	svg.setAttribute('style', `background-color: hsl(${hueBg.valueAsNumber}, ${satBg.valueAsNumber}%, ${lightBg.valueAsNumber}%)`);
}

/**
 * @function toggle
 * @description toggle a menu-group
 * @param {Event} event
*/
function toggle(event) {
	summary.forEach(node => {
		if (node !== event.target) node.parentNode.open = false;
	});
}

/* Init */
const summary = app.querySelectorAll('summary');
summary.forEach(node => node.addEventListener('click', toggle));
app.addEventListener('input', (event) => { if (event.target) render(event.target); });

const preset = {
  numRings: 12,
  dotSize: 12,
  dotsPerRing: 7,
  spread: 36,
  randomRadius: 0,
  randomDotSize: 0,
  hueMin: 25,
  hueMax: 50,
  satMin: 50,
  satMax: 90,
  lightMin: 60,
  lightMax: 90,
  hueBg: 248,
  satBg: 50,
  lightBg: 25
}
loadPreset(preset);
              
            
!
999px

Console