Impostazioni

Questo pattern mostra come creare un componente delle impostazioni reattivo, che supporti più input del dispositivo e funzioni su più browser.

Articolo completo · Video su YouTube · Fonte su GitHub

<main>
 
<h1>Settings</h1>
 
 
<form>

   
<section>
     
<header>
       
<h2>Sound & vibration</h2>
       
<small>Adjust system volume channels</small>
     
</header>
     
     
<fieldset>
       
       
<div class="fieldset-item">
         
<picture aria-hidden="true">
           
<svg viewBox="0 0 24 24">
             
<title>A note icon</title>
             
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
           
</svg>
         
</picture>
         
<div class="input-stack">
           
<label
             
for="media-volume"
             
id="media-volume"
             
aria-hidden="true">
                Media volume
           
</label>
           
<input
             
name="media-volume"
             
aria-labelledby="media-volume"
             
type="range"
             
value="3"
             
max="10"
             
style="--track-fill: 30%"
           
>
         
</div>
       
</div>

       
<div class="fieldset-item">
         
<picture aria-hidden="true">
           
<title>A phone icon</title>
           
<svg viewBox="0 0 24 24">
             
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
           
</svg>
         
</picture>
         
<div class="input-stack">
           
<label for="call-volume" id="call-volume" aria-hidden="true">Call volume</label>
           
<input
             
name="call-volume"
             
aria-labelledby="call-volume"
             
type="range"
             
value="7"
             
max="10"
             
style="--track-fill: 70%"
           
>
         
</div>
       
</div>

       
<div class="fieldset-item">
         
<picture aria-hidden="true">
           
<svg viewBox="0 0 24 24">
             
<title>A bell icon</title>
             
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>
           
</svg>
         
</picture>
         
<div class="input-stack">
           
<label for="ring-volume" id="ring-volume" aria-hidden="true">Ring volume</label>
           
<input
             
name="ring-volume"
             
aria-labelledby="ring-volume"
             
type="range"
             
value="5"
             
max="10"
             
style="--track-fill: 50%"
           
>
         
</div>
       
</div>

       
<div class="fieldset-item">
         
<picture aria-hidden="true">
           
<svg viewBox="0 0 24 24">
             
<title>An alarm clock icon</title>
             
<path d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/>
           
</svg>
         
</picture>
         
<div class="input-stack">
           
<label for="alarm-volume" id="alarm-volume" aria-hidden="true">Alarm volume</label>
           
<input
             
name="alarm-volume"
             
aria-labelledby="alarm-volume"
             
type="range"
             
value="8"
             
max="10"
             
style="--track-fill: 80%"
           
>
         
</div>
       
</div>
       
     
</fieldset>
   
</section>

   
<section>
     
<header>
       
<h2>Notifications</h2>
       
<small>Turn specific channels on/off</small>
     
</header>
     
     
<fieldset>

       
<div class="fieldset-item">
         
<input
           
type="checkbox"
           
checked
           
id="text-notifications"
           
name="text-notifications"
         
>
         
<div class="input-stack">
           
<label for="text-notifications">
             
<h3>Text Messages</h3>
             
<small>Get notified about all text messages sent to your device</small>
           
</label>
         
</div>
       
</div>

       
<div class="fieldset-item">
         
<input
           
type="checkbox"
           
id="voice-notifications"
           
name="voice-notifications"
         
>
         
<div class="input-stack">
           
<label for="voice-notifications">
             
<h3>Voice Mail</h3>
             
<small>Get notified about all voice messages sent to your device</small>
           
</label>
         
</div>
       
</div>

       
<div class="fieldset-item">
         
<input
           
type="checkbox"
           
id="email-notifications"
           
name="email-notifications"
         
>
         
<div class="input-stack">
           
<label for="email-notifications">
             
<h3>Emails</h3>
             
<small>Get notified about all text messages to your device</small>
           
</label>
         
</div>
       
</div>

     
</fieldset>
   
</section>

 
</form>
</main>

       
@custom-media --motionOK (prefers-reduced-motion: no-preference);

:root {
 
--surface1: lch(10 0 0);
 
--surface2: lch(15 0 0);
 
--surface3: lch(20 0 0);
 
--surface4: lch(25 0 0);

 
--text1: lch(95 0 0);
 
--text2: lch(75 0 0);

 
--brand: lch(64 20 237);
 
--brand-bg1: lch(70 64 349);
 
--brand-bg2: lch(60 84 300);

 
--brand-bg-gradient: linear-gradient(
    to bottom
,
    var
(--brand-bg1),
    var
(--brand-bg2)
 
);

 
--thumb-highlight-color: lch(100 0 0 / 20%);

 
--space-xxs: .25rem;
 
--space-xs:  .5rem;
 
--space-sm:  1rem;
 
--space-md:  1.5rem;
 
--space-lg:  2rem;
 
--space-xl:  3rem;
 
--space-xxl: 6rem;
 
 
--isLTR: 1;
 
--isRTL: -1;
 
 
&:dir(rtl) {
   
--isLTR: -1;
   
--isRTL: 1;
 
}

 
@media (prefers-color-scheme: light) {
   
& {
     
--surface1: lch(90 0 0);
     
--surface2: lch(100 0 0);
     
--surface3: lch(98 0 0);
     
--surface4: lch(85 0 0);

     
--text1: lch(20 0 0);
     
--text2: lch(40 0 0);

     
--brand: lch(64 40 237);
     
--brand-bg1: lch(50 64 349);
     
--brand-bg2: lch(40 84 300);

     
--thumb-highlight-color: lch(0 0 0 / 20%);
   
}
 
}
}

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

body
{
 
min-block-size: 100%;
 
min-inline-size: 100%;

 
box-sizing: border-box;
 
margin: 0;
 
padding-block: var(--space-xs);

 
background: var(--surface1);
 
color: var(--text1);
 
font-family: system-ui, sans-serif;
}

h1
,h2,h3 {
 
margin: 0;
 
font-weight: 500;
}

main
{
 
display: grid;
 
gap: var(--space-xl);
 
place-content: center;
 
padding: var(--space-sm);

 
@media (width >= 540px) {
   
padding: var(--space-lg);
 
}

 
@media (width >= 800px) {
   
padding: var(--space-xl);
 
}
}

form
{
 
max-width: 89vw;
 
display: grid;
 
gap: var(--space-xl) var(--space-xxl);
 
--repeat: auto-fit;
 
grid-template-columns:
    repeat
(var(--repeat), minmax(min(10ch, 100%), 35ch));
 
align-items: flex-start;

 
@media (orientation: landscape) and (width >= 640px) {
   
--repeat: 2;
 
}
}

section
{
 
display: grid;
 
gap: var(--space-md);
}

header
{
 
display: grid;
 
gap: var(--space-xxs);
}

fieldset
{
 
border: 1px solid var(--surface4);
 
background: var(--surface4);
 
padding: 0;
 
margin: 0;
 
display: grid;
 
gap: 1px;
 
border-radius: var(--space-sm);
 
overflow: hidden;
 
transition: box-shadow .3s ease;

 
&:focus-within {
   
box-shadow: 0 5px 20px -10px hsl(0 0% 0% / 50%);
 
}
}

input
[type="range"] {
 
--track-height: .5ex;
 
--track-fill: 0%;
 
--thumb-size: 3ex;
 
--thumb-offset: -1.25ex;
 
--thumb-highlight-size: 0px;

 
display: block;
 
inline-size: 100%;
 
margin: 1ex 0;
 
appearance: none;
 
background: transparent;
 
outline-offset: 5px;

 
@media (hover: none) {
   
--thumb-size: 30px;
   
--thumb-offset: -14px;
 
}

 
&::-webkit-slider-runnable-track {
   
appearance: none;
   
block-size: var(--track-height);
   
border-radius: 5ex;
   
background:
      linear-gradient
(
        to right
,
        transparent var
(--track-fill),
        var
(--surface1) 0%
     
),
      var
(--brand-bg-gradient) fixed;
 
}

 
&::-moz-range-track {
   
appearance: none;
   
block-size: var(--track-height);
   
border-radius: 5ex;
   
background:
      linear-gradient
(
        to right
,
        transparent var
(--track-fill),
        var
(--surface1) 0%
     
),
      var
(--brand-bg-gradient) fixed;
 
}

 
&::-webkit-slider-thumb {
   
appearance: none;
   
cursor: ew-resize;
   
border: 3px solid var(--surface3);
   
block-size: var(--thumb-size);
   
inline-size: var(--thumb-size);
   
margin-block-start: var(--thumb-offset);
   
border-radius: 50%;
   
background: var(--brand-bg-gradient) fixed;
   
box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);
   
   
@media (--motionOK) {
     
transition: box-shadow .1s ease;
   
}

   
@nest .fieldset-item:focus-within & {
     
border-color: var(--surface2);
   
}
 
}

 
&::-moz-range-thumb {
   
appearance: none;
   
cursor: ew-resize;
   
border: 3px solid var(--surface3);
   
block-size: var(--thumb-size);
   
inline-size: var(--thumb-size);
   
margin-block-start: var(--thumb-offset);
   
border-radius: 50%;
   
background: var(--brand-bg-gradient) fixed;
   
box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);

   
@media (--motionOK) {
     
transition: box-shadow .1s ease;
   
}

   
@nest .fieldset-item:focus-within & {
     
border-color: var(--surface2);
   
}
 
}

 
&:is(:hover,:active) {
   
--thumb-highlight-size: 10px;
 
}
}

input
[type="checkbox"] {
 
inline-size: var(--space-sm);
 
block-size: var(--space-sm);
 
margin: 0;
 
outline-offset: 5px;
 
accent-color: var(--brand);
 
position: relative;
 
transform-style: preserve-3d;
 
cursor: pointer;

 
&:hover::before {
   
--thumb-scale: 1;
 
}

 
@media (hover: none) {
   
inline-size: var(--space-md);
   
block-size: var(--space-md);
 
}

 
&::before {
   
--thumb-scale: .01;
   
--thumb-highlight-size: var(--space-xl);

   
content: "";
   
inline-size: var(--thumb-highlight-size);
   
block-size: var(--thumb-highlight-size);
   
clip-path: circle(50%);
   
position: absolute;
   
inset-block-start: 50%;
   
inset-inline-start: 50%;
   
background: var(--thumb-highlight-color);
   
transform-origin: center center;
   
transform:
      translateX
(calc(var(--isRTL) * 50%))
      translateY
(-50%)
      translateZ
(-1px)
      scale
(var(--thumb-scale))
   
;
   
will-change: transform;

   
@media (--motionOK) {
     
transition: transform .2s ease;
   
}
 
}
}

.fieldset-item {
 
background: var(--surface3);
 
transition: background .2s ease;

 
display: grid;
 
grid-template-columns: var(--space-lg) 1fr;
 
gap: var(--space-md);

 
padding-block: var(--space-sm);
 
padding-inline: var(--space-md);

 
@media (width >= 540px) {
   
grid-template-columns: var(--space-xxl) 1fr;
   
gap: var(--space-xs);
   
padding-block: var(--space-md);
   
padding-inline: 0 var(--space-xl);
 
}

 
&:focus-within {
   
background: var(--surface2);

   
& svg {
     
fill: white;
   
}

   
& picture {
     
clip-path: circle(50%);
     
background: var(--brand-bg-gradient) fixed;
   
}
 
}

 
& > :is(.input-stack, label) {
   
display: grid;
   
gap: var(--space-xs);
 
}

 
& > .input-stack > label {
   
display: contents;
 
}

 
& > picture {
   
block-size: var(--space-xl);
   
inline-size: var(--space-xl);
   
clip-path: circle(40%);
   
display: inline-grid;
   
place-content: center;
   
background: var(--surface3) fixed;

   
@media (--motionOK) {
     
transition: clip-path .3s ease;
   
}
 
}

 
& svg {
   
fill: var(--text2);
   
block-size: var(--space-md);
 
}

 
& > :is(picture, input[type="checkbox"]) {
   
place-self: center;
 
}
}

small
{
 
color: var(--text2);
 
line-height: 1.5;
}
       

       
const form = document.querySelector('form')
const sliders = document.querySelectorAll('input[type="range"]')

const rangeToPercent = slider => {
 
const max = slider.getAttribute('max') || 10
 
const percent = slider.value / max * 100

 
return `${parseInt(percent)}%`
}

sliders
.forEach(slider => {
  slider
.style.setProperty('--track-fill', rangeToPercent(slider))

  slider
.addEventListener('input', e => {
    e
.target.style.setProperty('--track-fill', rangeToPercent(e.target))
 
})
})

form
.addEventListener('input', e => {
 
const formData = Object.fromEntries(new FormData(form))
  console
.table(formData)
})