skip to Main Content

I’m new to python/flask, and have made a simple app which collects user input for 5 variables, and display an output that’s dependant on the combination of those six variables. I’ve made a simple webpage with the following code to collect the user’s input via clickable buttons:

function setValue(inputId, value, button) {
  document.getElementById(inputId + '_input').value = value;
}
body {
  width: 35em;
  margin: 0 auto;
  font-family: Tahoma, Verdana, Arial, sans-serif;
}

.button {
  align-self: center;
  background-color: #fff;
  background-image: none;
  background-position: 0 90%;
  background-repeat: repeat no-repeat;
  background-size: 4px 3px;
  border-radius: 15px 225px 255px 15px 15px 255px 225px 15px;
  border-style: solid;
  border-width: 2px;
  box-shadow: rgba(0, 0, 0, .2) 15px 28px 25px -18px;
  box-sizing: border-box;
  color: #41403e;
  cursor: pointer;
  display: inline-block;
  font-family: Neucha, sans-serif;
  font-size: 1rem;
  line-height: 23px;
  outline: none;
  padding: .75rem;
  text-decoration: none;
  transition: all 235ms ease-in-out;
  border-bottom-left-radius: 15px 255px;
  border-bottom-right-radius: 225px 15px;
  border-top-left-radius: 255px 15px;
  border-top-right-radius: 15px 225px;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

.button:hover {
  box-shadow: rgba(0, 0, 0, .3) 2px 8px 8px -5px;
  transform: translate3d(0, 2px, 0);
}

.button:focus {
  box-shadow: rgba(0, 0, 0, .3) 2px 8px 4px -6px;
}
<h1>Rhesus Genotype Analyzer</h1>
<form method="POST" action="/">
  <label for="D">D:</label>

  <button type="button" class="button" onclick="setValue('D', 'pos', this)">Pos</button>
  <button type="button" class="button" onclick="setValue('D', 'neg', this)">Neg</button>
  

  <label for="C">C:</label>
  <button type="button" class="button" onclick="setValue('C', 'pos', this)">Pos</button>
  <button type="button" class="button" onclick="setValue('C', 'neg', this)">Neg</button>
  <br><br>

  <label for="E">E:</label>
  <button type="button" class="button" onclick="setValue('E', 'pos', this)">Pos</button>
  <button type="button" class="button" onclick="setValue('E', 'neg', this)">Neg</button>
  <br><br>

  <label for="c">c:</label>
  <button type="button" class="button" onclick="setValue('c', 'pos', this)">Pos</button>
  <button type="button" class="button" onclick="setValue('c', 'neg', this)">Neg</button>
  <br><br>

  <label for="e">e:</label>
  <button type="button" class="button" onclick="setValue('e', 'pos', this)">Pos</button>
  <button type="button" class="button" onclick="setValue('e', 'neg', this)">Neg</button>
  <br><br>

  <input type="hidden" id="D_input" name="D">
  <input type="hidden" id="C_input" name="C">
  <input type="hidden" id="E_input" name="E">
  <input type="hidden" id="c_input" name="c">
  <input type="hidden" id="e_input" name="e">

  <input type="submit" value="Submit">

The issue I have is that only one of the 10 buttons displays the ‘focus’ state. Is it possible to treat each row of buttons independently? I.e., allow the ‘D’ pos or neg button to be ‘focused’, and allow the ‘E’ pos or neg to be ‘focused’ too, so that the user can see what they’ve selected?

Thanks a lot for any help!

2

Answers


  1. You can update the className of the selected buttons and separate the selected ones by className. In this example, I added active className to the clicked buttons and showed their background as green. When the user clicks on the selected button again, remove the active className and set it to default properties.

    function setValue(inputId, value, button) {
        document.getElementById(inputId + '_input').value = value;
         
        if(button.className.includes('active')){
            button.className = ('button')
        } 
        else {
            button.className += ' active';
        }
    }
    body {
      width: 35em;
      margin: 0 auto;
      font-family: Tahoma, Verdana, Arial, sans-serif;
    }
    
    .button {
      align-self: center;
      background-color: #fff;
      background-image: none;
      background-position: 0 90%;
      background-repeat: repeat no-repeat;
      background-size: 4px 3px;
      border-radius: 15px 225px 255px 15px 15px 255px 225px 15px;
      border-style: solid;
      border-width: 2px;
      box-shadow: rgba(0, 0, 0, .2) 15px 28px 25px -18px;
      box-sizing: border-box;
      color: #41403e;
      cursor: pointer;
      display: inline-block;
      font-family: Neucha, sans-serif;
      font-size: 1rem;
      line-height: 23px;
      outline: none;
      padding: .75rem;
      text-decoration: none;
      transition: all 235ms ease-in-out;
      border-bottom-left-radius: 15px 255px;
      border-bottom-right-radius: 225px 15px;
      border-top-left-radius: 255px 15px;
      border-top-right-radius: 15px 225px;
      user-select: none;
      -webkit-user-select: none;
      touch-action: manipulation;
    }
    
    .button:hover {
      box-shadow: rgba(0, 0, 0, .3) 2px 8px 8px -5px;
      transform: translate3d(0, 2px, 0);
    }
    
    .button:focus {
      box-shadow: rgba(0, 0, 0, .3) 2px 8px 4px -6px;
    }
    
    .active { /* Added active property here */
      background-color: green;
    }
    <body>
    <h1>Rhesus Genotype Analyzer</h1>
    <form method="POST" action="/">
      <label for="D">D:</label>
    
      <button type="button" class="button" onclick="setValue('D', 'pos', this)">Pos</button>
      <button type="button" class="button" onclick="setValue('D', 'neg', this)">Neg</button>
      
    
      <label for="C">C:</label>
      <button type="button" class="button" onclick="setValue('C', 'pos', this)">Pos</button>
      <button type="button" class="button" onclick="setValue('C', 'neg', this)">Neg</button>
      <br><br>
    
      <label for="E">E:</label>
      <button type="button" class="button" onclick="setValue('E', 'pos', this)">Pos</button>
      <button type="button" class="button" onclick="setValue('E', 'neg', this)">Neg</button>
      <br><br>
    
      <label for="c">c:</label>
      <button type="button" class="button" onclick="setValue('c', 'pos', this)">Pos</button>
      <button type="button" class="button" onclick="setValue('c', 'neg', this)">Neg</button>
      <br><br>
    
      <label for="e">e:</label>
      <button type="button" class="button" onclick="setValue('e', 'pos', this)">Pos</button>
      <button type="button" class="button" onclick="setValue('e', 'neg', this)">Neg</button>
      <br><br>
    
      <input type="hidden" id="D_input" name="D">
      <input type="hidden" id="C_input" name="C">
      <input type="hidden" id="E_input" name="E">
      <input type="hidden" id="c_input" name="c">
      <input type="hidden" id="e_input" name="e">
    
      <input type="submit" value="Submit">
    
    </body>

    Hope, it’s clear and useful

    Login or Signup to reply.
  2. One approach is as follows, though this answer is reliant on the :has() CSS pseudo-class function, though this is – now – well supported in modern browsers, this approach may mean you’ll need to use JavaScript depending on the browsers employed by your users.

    That said, the following meets your needs (I think) and uses only HTML and CSS to do so; explanatory comments are in the code:

    /*
      a simple reset, in which I ensure that all elements are sized
      in the same way, following the border-box algorithm which
      includes assigned padding, margins, and border-width within
      the assigned size of the element. I'm also setting the font
      of the document to remove browser defaults:
    */
    *,
    ::before,
    ::after {
      box-sizing: border-box;
      font-family: system-ui;
      font-size: 1rem;
      font-weight: 400;
      margin: 0;
      padding: 0;
    }
    
    /* forcing the <html> element to occupy the full size of the
       viewport's block-axis (the axis upon which 'blocks' are
       positioned according to the language choice of the user): */
    html {
      block-size: 100%;
    }
    
    /* defining some custom properties to help lay out the contents,
       and its spacing: */
    body {
      --space-s: calc(var(--space) * 0.5);
      --space: 0.5rem;
      --space-l: calc(var(--space) * 2);
      /* 1vb is 1% of the element's size on the block-axis, therefore
         this rule sizes the body to take up the full size of the
         viewport: */
      min-block-size: 100vb;
      /* setting the padding of the element, using the CSS var()
         function along with the defined CSS custom property: */
      padding: var(--space-l);
    }
    
    main {
      /* as above, sizing the block-axis of the element to take 100%
         of the available space: */
      min-block-size: 100%;
      border: 1px solid currentColor;
      padding: var(--space-l);
    }
    
    h1 {
      /* styling the font of the <h1> after forcing all
         fonts (earlier) to be only 1rem, here we double
         that size to emphasise the heading: */
      font-size: 2rem;
      /* setting a margin on the block-end (the side of
         the element that would be nearest the next block-
         level sibling element) to be in proportion to
         the font-size of the current element: */
      margin-block-end: 0.5em;
    }
    
    h2::after {
      content: ':';
    }
    
    form {
      /* using grid layout: */
      display: grid;
      /* setting the gap between adjacent elements within
         the grid: */
      gap: var(--space-l);
    }
    
    /* styling any <button> element within any <form> that
       has any :invalid form-elements (<input>, <textarea>...): */
    form:has(:invalid) button {
      /* highlighting that clicking the button is disallowed: */
      cursor: not-allowed;
      /* reducing the opacity to imply that the element is
         disabled: */
      opacity: 0.5;
      /* it would have been possible to use:
            pointer-events: none;
         but unfortunately that prevents the :hover pseudo-class
         from matching, which means the cursor wouldn't be
         changed. This is the choice I made, you may feel
         that preventing user-action on the <button> is worth
         that limitation. */
    }
    
    fieldset {
      --indicator: hsl(0deg 60% 40% / 0.8);
      align-items: center;
      display: flex;
      flex-flow: row wrap;
      gap: var(--space-l);
      isolation: isolate;
      padding-block: var(--space);
      padding-inline: var(--space-l);
      position: relative;
    }
    
    fieldset::after {
      /* I freely admit I went a bit overboard...
         here we're using a linear-gradient as a background image,
         the gradient running 90degrees (to the right, 0degrees is
         vertical), we have transparent from 0 running to 70% of the
         background-size, after which we use the colour assigned to
         the custom CSS property --indicator or, if that property isn't
         defined, the default colour of transparent.
         The background is positioned at 100% (x-axis) and 50% (y-axis),
         and sized 100% in both horizontal and vertical axes (respectively),
         and we set no-repeat so that the image doesn't repeat: */
      background: linear-gradient(90deg, transparent 0 70%, var(--indicator, transparent)) 100% 50% / 100% 100% no-repeat;
      /* to occlude the error gradient we use the clip-path, which allows
         us to limit the visibility of the element according to the
         shape we define. We're using the polygon() function here,
         which takes a list of comma-separated positions (for x and y
         respectively), to define a number of points; within the space
         formed by those points the content is visible. Outside, the
         content is hidden ('clipped'): */
      clip-path:
        polygon(
          100% 0,
          100% 0,
          100% 100%,
          100% 100%
        );
      /* content is a mandatory property to have the pseudo-element
         be rendered, an empty string is valid 'content' for this: */
      content: '';
      /* using inset to specify the position of the pseudo-element in
         reference to its containing block, here it's positioned 0
         distance from each edge (top, right, bottom, left respectively)
       */
      inset: 0;
      position: absolute;
      /* we're transitioning the clip-path property, over 500ms and
         with a linear easing function: */
      transition: clip-path 500ms linear;
      /* to position the pseudo-element behind all content of its
         containing block ancestor, appearing in front of only
         that element's background: */
      z-index: -1;
    }
    
    /* styling the ::after pseudo-element of any <fieldset> that contains
       a :user-invalid form-element: */
    fieldset:has(:user-invalid)::after {
      clip-path: polygon( 0 0, 100% 0, 100% 100%, 0 100%);
    }
    
    label {
      /* using a non-'static' (the default) value for the
         position property, in order that this element becomes
         the containing block for its descendants: */
      position: relative;
    }
    
    /* the element that contains the text associated with each
       <input> element: */
    .labelText {
      /* various CSS custom properties: */
      --base-h: 0deg;
      --base-s: 20%;
      --base-l: 30%;
      --base-z-pos: 0px;
      --border-color: hsl(var(--base-h) var(--base-s) var(--base-l) / 1);
      --offset-x: 0px;
      --offset-y: 6px;
      --spread: 0.3rem;
      /* to ensure that the element is sized to have equal size on both
         its block, and inline, axes: */
      aspect-ratio: 1;
      /* a subtle background, I left it in but it's not all that visible: */
      background: repeating-linear-gradient(135deg, snow 0 0.5rem, azure 0.5rem 0.75rem);
      border: 2px solid var(--border-color);
      /* defining the block-size of the element, which will influence the
         inline-size via the aspect-ratio property: */
      block-size: 3.5rem;
      display: grid;
      /* using drop-shadow in place of box-shadow, as it's a little more accurate
         (if you remove the background of this element, the visible content of the
         element generates an accurate shadow, whereas box-shadow gives a shadow to
         the outer shape of the element, effectively just a rectangular polygon): */
      filter: drop-shadow(var(--offset-x) var(--offset-y) var(--spread) var(--border-color));
      /* centering the content within the element: */
      place-content: center;
      /* using perspective and translateZ() to move the element somewhat
         faithfully in 3d 'space': */
      transform: perspective(500px) translateZ(var(--base-z-pos));
      /* required to enable any sort of accuracy within the 3d space: */
      transform-style: preserve-3d;
      /* defining a transition to apply to every (animatable) property: */
      transition: all 300ms ease-in-out;
      /* defining the precise properties to transition: */
      transition-property: transform, filter;
    }
    
    /* updating CSS properties for:
       any .labelText element when hovered: */
    .labelText:hover,
    /* any .labelText element that is immediately preceded
       by an <input> which is in any of the states listed
       within the :is() pseudo-class function: */
    input:is(:active, :focus, :checked) + .labelText {
      --base-h: 120deg;
      --base-z-pos: -50px;
    }
    
    /* as above, but to specify different properties that
       I didn't want available to the .labelText:hover: */
    input:is(:active, :focus, :checked) + .labelText {
      --base-s: 40%;
      --base-l: 60%;
      --base-z-pos: -80px;
    }
    
    /* we're hiding the <input>, since we're "replacing" it
       with the <span>: */
    input {
      /* scaling it very, very, very small: */
      scale: 0.01;
      /* using absolute position to remove the element from
         the document flow: */
      position: absolute;
    }
    
    .error {
      background-color: snow;
      border: 2px solid currentColor;
      /* using a large border-radius to have the browser
         style the borders into a 'pill' shape, with
         rounded ends: */
      border-radius: 5rem;
      /* using inset to position the element with reference
         to the containing block, here the 'auto' values
         are equivalent to not setting a property and allowing
         the browser to calculate it, but all values must be
         provided so we have to specify a value; -150% positions
         the pseudo-element's right side 150% outside of the
         containing block. Negative numbers move away from the
         center of the containing block, positive numbers
         move towards the center (on the relevant axis): */
      inset: auto -150% auto auto;
      padding-block: var(--space-s);
      padding-inline: var(--space);
      position: absolute;
      transition: inset 500ms linear;
    }
    
    /* :user-invalid matches form-elements that have been given,
       or retained invalid values due to the action/inaction of
       the user; the pseudo-class only matches elements after
       the user attempts to submit the form; with this selector
       we're seleing any .error message within a <fieldset> that
       itself contains an <input> which is invalid following the
       user's attempt to submit the form: */
    fieldset:has(input:user-invalid) .error {
      inset: auto 20% auto auto;
    }
    <main>
      <h1>Rhesus Genotype Analyzer</h1>
      <form method="POST" action="/">
        <fieldset>
          <!-- I opted to use an <h2> element to label the grouped elements,
               as a <legend> wouldn't be styled as you seem to wish: -->
          <h2>D</h2>
          <!-- using <label> elements to associate the text, in the nested <span>
               with the <input>, no "for" attribute is required as the relationship
               is implicit due to the <input> being nested within the <label> -->
          <label>
            <!-- the <span> follows the <input> in order to use
                 the validity, and user-interactivity, pseudo-
                 classes of the <input> to style the adjacent
                 <span>: -->
            <input type="radio" value="pos" name="D" required="">
            <span class="labelText">pos</span>
          </label>
          <label>
            <input type="radio" value="neg" name="D">
            <span class="labelText">neg</span>
          </label>
          <!-- using a <span> to contain an error message in the event that the
               user tries to submit an incomplete form: -->
          <span class="error">This field is mandatory, please select one of the options.</span>
        </fieldset>
    
        <fieldset>
          <h2>C</h2>
          <label>
            <input type="radio" value="pos" name="C" required="">
            <span class="labelText">pos</span>
          </label>
          <label>
            <input type="radio" value="neg" name="C">
            <span class="labelText">neg</span>
          </label>
          <span class="error">This field is mandatory, please select one of the options.</span>
        </fieldset>
    
        <fieldset>
          <h2>E</h2>
          <label>
            <input type="radio" value="pos" name="E" required="">
            <span class="labelText">pos</span>
          </label>
          <label>
            <input type="radio" value="neg" name="E">
            <span class="labelText">neg</span>
          </label>
          <span class="error">This field is mandatory, please select one of the options.</span>
        </fieldset>
    
        <fieldset>
          <h2>c</h2>
          <label>
            <input type="radio" value="pos" name="c" required="">
            <span class="labelText">pos</span>
          </label>
          <label>
            <input type="radio" value="neg" name="c">
            <span class="labelText">neg</span>
          </label>
          <span class="error">This field is mandatory, please select one of the options.</span>
        </fieldset>
    
        <fieldset>
          <h2>e</h2>
          <label>
            <input type="radio" value="pos" name="e" required="">
            <span class="labelText">pos</span>
          </label>
          <label>
            <input type="radio" value="neg" name="e">
            <span class="labelText">neg</span>
          </label>
          <span class="error">This field is mandatory, please select one of the options.</span>
        </fieldset>
        
        <!-- rather than use an <input> to submit the form, I opted to use
             a <button>; the default action of a <button> within a <form> is
             to submit the form, which is why there's no explicit 'submit'
             attribute: -->
        <button>Submit</button>
      </form>
    </main>

    JS Fiddle demo.

    References:

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