This tutorial will guide you through creating an image editor UI design using HTML, CSS, and JavaScript. We’ll build a visually appealing interface with interactive controls to adjust image contrast, saturation, brightness, blur, and hue. The end result will be a responsive and engaging image manipulation tool.
Step 1: Setting up the HTML Structure
Creating the Main Containers
First, we need to establish the basic HTML structure. This involves creating several divs to hold the image and the control elements. Each scroller represents a different image adjustment.
<div class="scroller-cover"></div>
<div class="scroll-center"></div>
<div class="scroller" data-level="1">
<div class="sizer" data-level="1">
<div class="scroller" data-level="2">
<div class="sizer" data-level="2">
<div class="scroller" data-level="3">
<div class="sizer" data-level="3">
<div class="scroller" data-level="4" data-no-initial-scroll>
<div class="sizer" data-level="4">
<div class="scroller" data-level="5" data-no-initial-scroll>
<div class="sizer" data-level="5">
<div id="image">
<div>
<!-- Change this to any image -->
<img src="https://images.unsplash.com/photo-1715604723676-7e7fb8607478?q=80&w=1200&h=1200&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="buttons">
<div data-mode="contrast">
<input type="radio" name="mode" value="contrast" checked/>
<div class="icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.37 11.84C29.1015 10.97 28.7464 10.1291 28.31 9.33001C27.6646 8.14402 26.8531 7.05635 25.9 6.10001C23.942 4.14221 21.4474 2.80899 18.7317 2.26891C16.016 1.72883 13.2012 2.00616 10.6431 3.06583C8.085 4.1255 5.89858 5.91991 4.3603 8.22217C2.82203 10.5244 2.00098 13.2311 2.00098 16C2.00098 18.7689 2.82203 21.4756 4.3603 23.7778C5.89858 26.0801 8.085 27.8745 10.6431 28.9342C13.2012 29.9939 16.016 30.2712 18.7317 29.7311C21.4474 29.191 23.942 27.8578 25.9 25.9C26.8531 24.9437 27.6646 23.856 28.31 22.67C28.7464 21.8709 29.1015 21.03 29.37 20.16C30.2131 17.4508 30.2131 14.5493 29.37 11.84ZM3.99997 16C3.99997 12.8174 5.26425 9.76516 7.51468 7.51472C9.76512 5.26429 12.8174 4.00001 16 4.00001V28C12.8174 28 9.76512 26.7357 7.51468 24.4853C5.26425 22.2348 3.99997 19.1826 3.99997 16Z" fill="currentColor"/>
</svg>
</div>
<div class="label">Contrast</div>
</div>
<div data-mode="saturation">
<input type="radio" name="mode" value="saturation"/>
<div class="icon"><svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 14C11.1046 14 12 13.1046 12 12C12 10.8954 11.1046 10 10 10C8.89543 10 8 10.8954 8 12C8 13.1046 8.89543 14 10 14Z" fill="currentColor"/>
<path d="M16 11C17.1046 11 18 10.1046 18 9C18 7.89543 17.1046 7 16 7C14.8954 7 14 7.89543 14 9C14 10.1046 14.8954 11 16 11Z" fill="currentColor"/>
<path d="M22 14C23.1046 14 24 13.1046 24 12C24 10.8954 23.1046 10 22 10C20.8954 10 20 10.8954 20 12C20 13.1046 20.8954 14 22 14Z" fill="currentColor"/>
<path d="M23 20C24.1046 20 25 19.1046 25 18C25 16.8954 24.1046 16 23 16C21.8954 16 21 16.8954 21 18C21 19.1046 21.8954 20 23 20Z" fill="currentColor"/>
<path d="M19 25C20.1046 25 21 24.1046 21 23C21 21.8954 20.1046 21 19 21C17.8954 21 17 21.8954 17 23C17 24.1046 17.8954 25 19 25Z" fill="currentColor"/>
<path d="M16.54 2.00004C14.6566 1.92734 12.7778 2.23571 11.0165 2.90665C9.25507 3.57759 7.64731 4.59729 6.28955 5.90463C4.93179 7.21196 3.852 8.78 3.11491 10.5147C2.37781 12.2495 1.9986 14.1152 2 16C1.99995 16.7414 2.1709 17.4727 2.49955 18.1372C2.8282 18.8017 3.30569 19.3814 3.8949 19.8313C4.4841 20.2812 5.16914 20.5891 5.89674 20.7311C6.62433 20.8731 7.37488 20.8454 8.09 20.65L9.21 20.34C9.65555 20.2184 10.1232 20.2013 10.5764 20.29C11.0296 20.3788 11.4563 20.571 11.8231 20.8516C12.1898 21.1323 12.4869 21.4938 12.691 21.9081C12.8952 22.3224 13.0009 22.7782 13 23.24V27C13 27.7957 13.3161 28.5588 13.8787 29.1214C14.4413 29.684 15.2044 30 16 30C17.8848 30.0014 19.7506 29.6222 21.4853 28.8851C23.22 28.148 24.7881 27.0683 26.0954 25.7105C27.4028 24.3527 28.4225 22.745 29.0934 20.9836C29.7643 19.2222 30.0727 17.3435 30 15.46C29.8549 11.9367 28.3902 8.59677 25.8968 6.10329C23.4033 3.60981 20.0633 2.14514 16.54 2.00004ZM24.65 24.31C23.5334 25.4791 22.1909 26.409 20.7039 27.0433C19.217 27.6776 17.6166 28.0031 16 28C15.7348 28 15.4804 27.8947 15.2929 27.7071C15.1054 27.5196 15 27.2653 15 27V23.24C15 21.914 14.4732 20.6422 13.5355 19.7045C12.5979 18.7668 11.3261 18.24 10 18.24C9.55065 18.2408 9.1034 18.3014 8.67 18.42L7.55 18.73C7.13168 18.8422 6.69316 18.8564 6.26844 18.7717C5.84373 18.687 5.44422 18.5056 5.10092 18.2416C4.75761 17.9776 4.47972 17.6381 4.2888 17.2493C4.09788 16.8606 3.99906 16.4331 4 16C3.99876 14.3838 4.32402 12.784 4.95625 11.2966C5.58848 9.80921 6.51467 8.46484 7.67923 7.34417C8.8438 6.2235 10.2227 5.34962 11.7333 4.77497C13.2439 4.20032 14.855 3.93674 16.47 4.00004C19.4772 4.15667 22.3198 5.42171 24.4491 7.551C26.5783 9.68028 27.8434 12.5229 28 15.53C28.0688 17.146 27.8072 18.759 27.2312 20.2704C26.6552 21.7818 25.7769 23.1598 24.65 24.32V24.31Z" fill="currentColor"/>
</svg></div>
<div class="label">Saturation</div>
</div>
<div data-mode="brightness">
<input type="radio" name="mode" value="brightness"/>
<div class="icon">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 2H17V5H15V2ZM27 15H30V17H27V15ZM15 27H17V30H15V27ZM2 15H5V17H2V15Z" fill="currentColor"/>
<path d="M5.44995 6.88318L6.86417 5.46896L8.98549 7.59028L7.57127 9.0045L5.44995 6.88318Z" fill="currentColor"/>
<path d="M22.9998 7.5813L25.1211 5.45998L26.5353 6.87419L24.414 8.99551L22.9998 7.5813Z" fill="currentColor"/>
<path d="M23.0022 24.4164L24.4164 23.0022L26.5377 25.1235L25.1235 26.5378L23.0022 24.4164Z" fill="currentColor"/>
<path d="M5.46997 25.13L7.58997 23L8.99997 24.42L6.87997 26.54L5.46997 25.13ZM16 8C14.4177 8 12.871 8.46919 11.5554 9.34824C10.2398 10.2273 9.21444 11.4767 8.60893 12.9385C8.00343 14.4003 7.84501 16.0089 8.15369 17.5607C8.46237 19.1126 9.2243 20.538 10.3431 21.6569C11.4619 22.7757 12.8874 23.5376 14.4392 23.8463C15.9911 24.155 17.5996 23.9965 19.0614 23.391C20.5232 22.7855 21.7727 21.7602 22.6517 20.4446C23.5308 19.129 24 17.5823 24 16C24 13.8783 23.1571 11.8434 21.6568 10.3431C20.1565 8.84285 18.1217 8 16 8ZM16 22C14.4087 22 12.8825 21.3679 11.7573 20.2426C10.6321 19.1174 9.99997 17.5913 9.99997 16C9.99997 14.4087 10.6321 12.8826 11.7573 11.7574C12.8825 10.6321 14.4087 10 16 10V22Z" fill="currentColor"/>
</svg>
</div>
<div class="label">Brightness</div>
</div>
<div data-mode="blur">
<input type="radio" name="mode" value="blur"/>
<div class="icon"><svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 24V22C16.7732 21.9331 17.498 21.5955 18.0468 21.0468C18.5955 20.498 18.9331 19.7732 19 19H21C20.9354 20.3052 20.3879 21.5398 19.4638 22.4638C18.5398 23.3879 17.3052 23.9354 16 24Z" fill="currentColor"/>
<path d="M16 28.0001C13.614 27.9971 11.3265 27.0479 9.63937 25.3607C7.9522 23.6736 7.00302 21.3861 7 19.0001C7.0538 17.2457 7.56914 15.5365 8.4941 14.0447L15.1528 3.4368C15.249 3.30173 15.376 3.19161 15.5234 3.11563C15.6708 3.03964 15.8342 3 16 3C16.1658 3 16.3292 3.03964 16.4766 3.11563C16.624 3.19161 16.751 3.30173 16.8472 3.4368L23.4761 13.9932C24.4168 15.4986 24.9425 17.2259 25 19.0001C24.997 21.3861 24.0478 23.6736 22.3606 25.3607C20.6735 27.0479 18.386 27.9971 16 28.0001ZM16 5.8484L10.2183 15.0563C9.47323 16.2412 9.05306 17.6014 9 19.0001C9 20.8566 9.7375 22.6371 11.0503 23.9498C12.363 25.2626 14.1435 26.0001 16 26.0001C17.8565 26.0001 19.637 25.2626 20.9497 23.9498C22.2625 22.6371 23 20.8566 23 19.0001C22.943 17.5817 22.5125 16.2035 21.752 15.0048L16 5.8484Z" fill="currentColor"/>
</svg>
</div>
<div class="label">Blur</div>
</div>
<div data-mode="hue">
<input type="radio" name="mode" value="hue"/>
<div class="icon"><svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26 4H6C5.46977 4.00066 4.96145 4.21159 4.58652 4.58652C4.21159 4.96145 4.00066 5.46977 4 6V26C4.00066 26.5302 4.21159 27.0386 4.58652 27.4135C4.96145 27.7884 5.46977 27.9993 6 28H26C26.5302 27.9993 27.0386 27.7884 27.4135 27.4135C27.7884 27.0386 27.9993 26.5302 28 26V6C27.9993 5.46977 27.7884 4.96145 27.4135 4.58652C27.0386 4.21159 26.5302 4.00066 26 4ZM6 26L26 6V26H6Z" fill="currentColor"/>
</svg>
</div>
<div class="label">Hue</div>
</div>
</div>
Step 2: Styling with CSS
Here’s the CSS code to style the elements, making the UI visually appealing and functional. This includes setting up the layout, scrollbars, and animations. Pay close attention to the use of CSS variables (custom properties) for easy customization.
:root {
--contrast: 1;
--saturate: 0;
--blur: 0;
--hue: 0deg;
--brightness: 1;
--img-size: 500px;
--controls-width: 480px;
--top-offset: -60px;
--img-from-top: calc(50vh + (var(--img-size) / 2) + var(--top-offset));
--scroll-height: 60px;
--ui-bg: #222;
--ui-bg-transparent: #22222200;
--buttons-row-height: 100px;
}
body {
background-color: var(--ui-bg);
font-family: sans-serif;
}
*::-webkit-scrollbar {
display: none;
}
* {
user-select: none;
}
body:has(input[name="mode"][value="contrast"]:checked) {
.scroller[data-level="1"] {
pointer-events: all;
}
.scroller[data-level="1"] .sizer[data-level="1"]::before {
opacity: 1;
}
[data-mode="contrast"] {
color: white;
}
}
body:has(input[name="mode"][value="saturation"]:checked) {
.scroller[data-level="2"] {
pointer-events: all;
}
.scroller[data-level="2"] .sizer[data-level="2"]::before {
opacity: 1;
}
[data-mode="saturation"] {
color: white;
}
}
body:has(input[name="mode"][value="brightness"]:checked) {
.scroller[data-level="3"] {
pointer-events: all;
}
.scroller[data-level="3"] .sizer[data-level="3"]::before {
opacity: 1;
}
[data-mode="brightness"] {
color: white;
}
}
body:has(input[name="mode"][value="blur"]:checked) {
.scroller[data-level="4"] {
pointer-events: all;
}
.scroller[data-level="4"] .sizer[data-level="4"]::before {
opacity: 1;
}
[data-mode="blur"] {
color: white;
}
}
body:has(input[name="mode"][value="hue"]:checked) {
.scroller[data-level="5"] {
pointer-events: all;
}
.scroller[data-level="5"] .sizer[data-level="5"]::before {
opacity: 1;
}
[data-mode="hue"] {
color: white;
}
}
.scroller {
height: var(--scroll-height);
width: var(--controls-width);
overflow-x: scroll;
scroll-timeline-axis: x;
position: fixed;
top: var(--img-from-top);
left: 0;
right: 0;
margin: auto;
pointer-events: none;
overflow-y: visible;
}
.scroller-cover {
position: absolute;
top: calc(var(--img-from-top));
pointer-events: none;
height: var(--scroll-height);
background-image: linear-gradient(to right, var(--ui-bg), var(--ui-bg-transparent) 30%, var(--ui-bg-transparent) 70%, var(--ui-bg));
z-index: 3;
content: '';
left: 0;
right: 0;
margin: auto;
width: var(--controls-width);
}
.scroller[data-level="1"] {
scroll-timeline-name: --scroller-1;
}
.scroller[data-level="2"] {
scroll-timeline-name: --scroller-2;
}
.scroller[data-level="3"] {
scroll-timeline-name: --scroller-3;
}
.scroller[data-level="4"] {
scroll-timeline-name: --scroller-4;
}
.scroller[data-level="5"] {
scroll-timeline-name: --scroller-5;
}
.sizer {
width: 200%;
animation-duration: 1ms;
animation-direction: alternate;
animation-timing-function: linear;
overflow: visible;
}
.scroll-center {
content: '';
position: fixed;
top: calc(var(--img-from-top) + var(--scroll-height) - 30px);
height: 30px;
width: 2px;
background-color: #FF5D5D;
left: -1px;
right: 0;
margin: auto;
display: block;
position: absolute;
z-index: 9;
pointer-events: none;
}
.sizer::before {
content: '';
position: absolute;
left: 50%;
margin: auto;
opacity: 0;
background-image: url("https://assets.codepen.io/215059/ticker-6_6.svg");
background-size: 100% 16px;
background-repeat: no-repeat;
background-position: 0 100%;
height: 20px;
width: 100%;
bottom: 0;
pointer-events: none;
}
.sizer[data-level="1"] {
animation-name: contrastAnimation;
animation-timeline: --scroller-1;
}
.sizer[data-level="2"] {
animation-name: saturateAnimation;
animation-timeline: --scroller-2;
}
.sizer[data-level="3"] {
animation-name: brightnessAnimation;
animation-timeline: --scroller-3;
}
.sizer[data-level="4"] {
animation-name: blurAnimation;
animation-timeline: --scroller-4;
}
.sizer[data-level="5"] {
animation-name: hueAnimation;
animation-timeline: --scroller-5;
}
#image {
width: var(--img-size);
height: var(--img-size);
position: fixed;
pointer-events: none;
top: calc(var(--img-from-top) - var(--img-size));
left: 0;
right: 0;
margin: auto;
display: flex;
background: var(--ui-bg);
align-items: center;
justify-content: center;
}
#image > div {
object-fit: contain;
width: 100%;
height: 100%;
margin: auto;
overflow: hidden;
text-align: center;
}
#image img {
max-width: 100%;
max-height: 100%;
margin: auto;
filter: contrast(var(--contrast)) saturate(var(--saturate)) brightness(var(--brightness)) blur(var(--blur)) hue-rotate(var(--hue));
}
.buttons {
position: fixed;
left: 0;
right: 0;
margin: auto;
top: calc(var(--img-from-top) + var(--scroll-height));
height: var(--buttons-row-height);
width: var(--controls-width);
z-index: 2;
display: flex;
justify-content: center;
}
.buttons > div {
flex-basis: 90px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
color: #949494;
padding-top: 12px;
position: relative;
flex-direction: column;
}
.buttons > div input {
appearance: none;
opacity: 1;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: block;
width: 100%;
height: 100%;
z-index: 1;
}
.buttons > div .icon {
width: 48px;
height: 48px;
background-color: #3B3B3B;
border-radius: 50%;
margin-bottom: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon svg {
width: 24px;
}
@keyframes contrastAnimation {
0% {
--contrast: 0.3;
}
50% {
--contrast: 1;
}
100% {
--contrast: 2;
}
}
@keyframes saturateAnimation {
0% {
--saturate: 0;
}
50% {
--saturate: 1;
}
100% {
--saturate: 10;
}
}
@keyframes blurAnimation {
0% {
--blur: 0px;
}
100% {
--blur: 30px;
}
}
@keyframes hueAnimation {
0% {
--hue: 0deg;
}
100% {
--hue: 360deg;
}
}
@keyframes brightnessAnimation {
0% {
--brightness: 0;
}
50% {
--brightness: 1;
}
100% {
--brightness: 2;
}
}
/* @property --contrast {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
@property --saturate {
syntax: "<number>";
inherits: false;
initial-value: 0;
} */
Step 3: Adding JavaScript Functionality
This section involves the JavaScript code that makes the UI interactive. This code handles the registration of custom CSS properties, default scroll positions, and mouse drag events for smoother control. It’s crucial for enabling the smooth interaction of the scrollbars with the image filters.
// Unfortunately not zero-JS as we need to register the CSS properties to use them in keyframe animations.
['--contrast', '--saturate', '--brightness'].forEach((property) => {
CSS.registerProperty({
name: property, syntax: "<number>", initialValue: 0, inherits: "false"
});
})
CSS.registerProperty({
name: '--blur', syntax: "<length>", initialValue: '0px', inherits: "false"
});
CSS.registerProperty({
name: '--hue', syntax: "<angle>", initialValue: '0deg', inherits: "false"
});
// Also we need to default the scroll for several modes to the half way point.
document.querySelectorAll('.scroller:not([data-no-initial-scroll])').forEach((s) => s.scrollLeft = s.scrollWidth / 4);
// The above is enough for the demo to work on touch devices
// However, for non-touch devices let's map mouse drag to scroll
document.querySelectorAll('.scroller').forEach((slider) => {
let mouseDown = false;
let startX, scrollLeft;
const startDragging = (e) => {
mouseDown = true;
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
}
const stopDragging = (e) => {
mouseDown = false;
}
const move = (e) => {
e.preventDefault();
if(!mouseDown) { return; }
const x = e.pageX - slider.offsetLeft;
const scroll = x - startX;
slider.scrollLeft = scrollLeft - scroll;
}
slider.addEventListener('mousemove', move, false);
slider.addEventListener('mousedown', startDragging, false);
slider.addEventListener('mouseup', stopDragging, false);
slider.addEventListener('mouseleave', stopDragging, false);
})
Step 4: Including Assets
Finally, Add draggable JS via CDN link before closing the body tag.
<script src='https://cdn.jsdelivr.net/npm/@shopify/draggable@1.1.3/build/umd/index.min.js'></script>
Congratulations! You’ve successfully created an Image Editor UI design using HTML, CSS, and JavaScript.







