skip to Main Content

I want to create a gradient effect using pseudo-element :before on an image that user would change by moving a slider input. I tried to get to it by following code, but no luck so far

var sliderValue = $('#range');

sliderValue.oninput = function(){
    var val1 = this.value;
    var val2 = 100-val1;     
    $('<style> #filter:before{linear-gradient(to top, rgba(255,255,255,1) '+val1+'%,rgba(0,0,0,0) '+val2+'%);}</style>').appendTo("#filter");
};
.slider
{
    -webkit-appearance:none;
    appearance:none;
    width: 100%;
    height: 10px;
    max-width: 400px;
    background-color: #ccc;
    margin: 0;
    padding: 0;
    outline: none;
    border-radius: 10px;
    cursor: pointer;
}
#filter {
    position:relative;
    float: left;
    max-height: 480px; 
    
}

#filter:before {
    content: "";
    position:absolute;
    top:0;
    left: 0;
    right: 0;
    bottom: 0;
    width:320px;
    height:100%;
    background: linear-gradient(to top, rgba(255,255,255,1) 15%,rgba(0,0,0,0) 22%);
    z-index: 1;
}
<div class="container">
   <input type="range" min="0" max="100" value="0" class="slider" id="range">    
   <div id="filter">  
    <img id="previewImg" src="img/dummy_320x480_ffffff_d002e2.png" alt="Placeholder" style="height: 100%; width:320px;">
   </div>
</div>  
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>

3

Answers


  1. There are a few fixes that need to be made to the code for the gradient to work properly:

    1. The selection of the input element must be done with jQuery’s $(…) function, instead of document.getElementById(…).
    2. The input element’s oninput function must be linked to jQuery’s input event.
    3. The slider value should be normalized from 0 to 1 instead of 0 to 100.
    4. The gradient CSS declaration must be corrected to use the correct notation for the color value (example: rgba(255,255,255,1) should be rgba(255,255,255,1.0)).

    With these fixes, the code would look like this:

    HTML:

    <div class="container">
      <input type="range" min="0" max="100" value="0" class="slider" id="range">    
      <div id="filter">
        <img id="previewImg" src="img/dummy_320x480_ffffff_d002e2.png" alt="Placeholder" style="height: 100%; width:320px;">
      </div>
    </div>
    

    jQuery:

    var sliderValue = $('#range');
    sliderValue.on('input', function() {
      var val1 = this.value / 100;
      var val2 = 1 - val1;
      $('<style> #filter:before {background: linear-gradient(to top, rgba(255,255,255,1.0) ' + val1*100 + '%, rgba(0,0,0,0) ' + val2*100 + '%);}</style>').appendTo("#filter");
    });
    

    CSS:

    .slider {
      -webkit-appearance: none;
      appearance: none;
      width: 100%;
      height: 10px;
      max-width: 400px;
      background-color: #ccc;
      margin: 0;
      padding: 0;
      outline: none;
      border-radius: 10px;
      cursor: pointer;
    }
    
    #filter {
      position: relative;
      float: left;
      max-height: 480px; 
    }
    
    #filter:before {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      width: 320px;
      height: 100%;
      background: linear-gradient(to top, rgba(255,255,255,1.0) 15%, rgba(0,0,0,0) 22%);
      z-index: 1;
    }
    

    Remember that the tag is used to declare CSS styles in the header of the HTML page, and should not be used to dynamically create styles in jQuery. In the code above, we used the appendTo method to add the dynamically created CSS style to the #filter element.

    Login or Signup to reply.
  2. Some problems with your code:

    • You tried to append the new style on every input. I’ve added an HTML element for the style which gets it’s content updated on every input;
    • The color was not showing for some reason. I’ve changed the rgba(0,0,0,0) to rgba(0,0,0,.1) so you’ll be able to see it
    • The #filter div was on top of the slider, that’s why you couldn’t activate it. The float left is gone and now it works.
    var sliderValue = $('#range'),
        filterStyle = $('#filter-before');
        
    sliderValue.on('input', function() {
      var val1 = this.value;
      var val2 = 100 - val1;
      filterStyle.html('#filter:before{background: linear-gradient(to top, rgba(255,255,255,1) ' + val1 + '%,rgba(0,0,0,.1) ' + val2 + '%);}');
    });
    .slider {
      -webkit-appearance: none;
      appearance: none;
      width: 100%;
      height: 10px;
      max-width: 400px;
      background-color: #ccc;
      margin: 0;
      padding: 0;
      outline: none;
      border-radius: 10px;
      cursor: pointer;
    }
    
    #filter {
      position: relative;
      max-height: 480px;
      width: 200px;
      height: 200px;
    }
    
    #filter:before {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      width: 320px;
      height: 100%;
      background: linear-gradient(to top, rgba(255, 255, 255, 1) 15%, rgba(0, 0, 0, 0) 22%);
      z-index: 1;
    }
    <style id="filter-before"></style>
    <div class="container">
      <input type="range" min="0" max="100" class="slider" id="range">
      <div id="filter">
        <img id="previewImg" src="img/dummy_320x480_ffffff_d002e2.png" alt="Placeholder" style="height: 100%; width:320px;">
      </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
    Login or Signup to reply.
  3. As noted in other answers, there were a number of problems with your initial code, primarily that you were trying to add a <style> element to the document on each occurrence of an input event fired on your sliderValue element. Unfortunately that sliderValue Object was a jQuery Object which had no oninput method.

    To bind an event-handler with a jQuery Object the on() method should be used:

    sliderValue.on('<eventName>', function(){
      // functionality
    });
    

    However, while other answers have shown improved jQuery approaches I thought I’d take the opportunity to show you how this might be managed via plain JavaScript and CSS custom-properties as follows (with explanatory comments in the code):

    // simple utility functions to minimise typing:
    const D = document,
      get = (selector, context = D) => context.querySelector(selector),
      getAll = (selector, context = D) => [...context.querySelectorAll(selector)];
      
    // creating a new Event to trigger an input-event on the elements, once
    // an event-handler is bound:
    let inputEvent = new Event('input'),
        // a named arrow function to handle the updates triggered by input-events
        // fired on the <input> elements; this takes a single argument supplied
        // automagically from EventTarget.addEventListener():
            updateGradient = (evt) => {
        // we cache the element to which the function was bound:
            let updated = evt.currentTarget,
            // we cache a reference to the element specified in the
            // <input> element's "data-updates" attribute-value:
                target = get(updated.dataset.updates);
            
        // we then use CSSStyleDeclaration.setProperty() to set/update the
        // named property - derived from the <input> element's "name"
        // property (from its "name" attribute), to the current value
        // of the changed <input>:
        target.style.setProperty(`--${updated.name}`, updated.value)
      },
      // we get an Array of all <input> elements with the "data-updates"
      // attribute:
      updaters = getAll('input[data-updates]');
    
    // using Array.prototype.forEach() we iterate over the Array
    // of nodes using an Arrow function:
    updaters.forEach(
      // passing in a reference to the current element/node of
      // the Array of elements/nodes:
      (el) => {
        // here we use EventTarget.addEventListener() to bind the
        // updateGradient() function as the event-handler for the
        // "input" event fired on the <input> elements:
        el.addEventListener('input', updateGradient);
        
        // here we use EventTarget.dispatchEvent() to fire the 'input'
        // event to trigger the function bound in the previous line:
        el.dispatchEvent(inputEvent);
      });
    /* setting a custom property on the :root element, to
       make this property available to the whole document: */
    :root {
      --spacing: 1rem;
    }
    
    /* a simple CSS reset to remove default margins, paddings
       and to force browsers to use the same sizing algorithm
       for all elements: */
    *,
    ::before,
    ::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    /* setting a consistent base-font for the document: */
    html,
    body {
      font-family: system-ui;
      font-size: 16px;
      font-weight: 400;
      /* using CSS logical properties to specify that the
         <html> and <body> elements should take a minimum
         of the full available size of the block axis of
         the document (the height in English): */
      min-block-size: 100%;
      /* specifying the padding on the block-axis: */
      padding-block: var(--spacing);
    }
    
    main {
      block-size: 100%;
      border: 1px solid #000;
      display: grid;
      gap: var(--spacing);
      /* CSS logical properties, this sets the size of the
         inline axis (perpendicular to the block-axis) using
         the clamp() function, which sets a preferred size
         of 80% with an absolute minimum size of 30rem and
         a maximum size of 1200px: */
      inline-size: clamp(30rem, 80%, 1200px);
      /* using 'auto' to center the element(s) on its
         inline-axis: */
      margin-inline: auto;
    }
    
    form {
      display: grid;
      gap: var(--spacing);
      padding: var(--spacing);
    }
    
    label {
      display: flex;
      gap: var(--spacing);
    }
    
    .labelText {
      /* allowing the .labelText element to grow to occupy
         all available size on its inline-axis (as defined by
         flex-direction) once siblings have been sized: */
      flex-grow: 1;
      /* aligning the text to the end of its inline-axis: */
      text-align: end;
    }
    
    .labelText::after {
      content: ':';
    }
    
    label > input {
      /* sizing the <input> to take 70% of the available
         inline space of its parent: */
      flex-basis: 70%;
    }
    
    .preview {
      /* defining the custom properties for this element and
         any descendants; note that the --color1 and --color2
         properties are declared using CSS Color Module Level 4
         syntax */
      --color1: rgba(255 255 255 / 1);
      --color2: rgba(0 0 0 / 1);
      --stop: 50;
      /* specifying that the element is sized so its inline-axis
         is twice that of its block axis: */
      aspect-ratio: 2;
      background-image:
        linear-gradient(
          180deg,
          var(--color1, lightskyblue)
          /* we use calc to multiply by 1% in order to convert
             from the unitless --stop property to a percentage,
             in order for it to be used in CSS: */
          calc(var(--stop) * 1%),
          var(--color2, lime)
          calc(var(--stop) * 1%)
        );
    }
    <main>
      <!-- here we use a <form> element to contain the various <input> elements
           that can modify the gradient; this way if JavaScript fails then the
           page can degrade somewhat gracefully (depending on your back-end
           solutions) and simply be submitted to load a new page with the updated
           settings: -->
      <form action="#" method="post">
        <label>
          <!-- a simple wrapper element for label-text: -->
          <span class="labelText">Color 1</span>
          <!-- an <input> of type "color", to allow the user to modify the color,
               note the 'name' attribute, the starting value and the data-*
               custom property, the attribute-value of which is the selector for
               the element that is updated via this <input>, and the initial
               value of the element is used to initialise the CSS property-values: -->
          <input type="color" name="color1" value="#000000" data-updates=".preview">
        </label>
        <label>
          <span class="labelText">Color 2</span>
          <input type="color" name="color2" value="#ff9900" data-updates=".preview">
        </label>
        <label>
          <span class="labelText">Gradient stop (%)</span>
          <input type="range" name="stop" min="0" max="100" value="25" data-updates=".preview">
        </label>
        <!-- if JavaScript is working then this button is removed via JavaScript
             and no page-loads will be triggered: -->
        <button type="submit">Submit</button>
      </form>
      <div class="preview"></div>
    </main>

    JS Fiddle demo.

    References:

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search