ساخت کامپوننت تغییر تم

یک نمای کلی از نحوه ساخت یک جزء سوئیچ تم تطبیقی ​​و در دسترس.

در این پست می‌خواهم فکری در مورد روشی برای ساخت یک جزء سوئیچ تم تیره و روشن به اشتراک بگذارم. نسخه ی نمایشی را امتحان کنید .

اندازه دکمه دمو برای مشاهده آسان افزایش یافته است

اگر ویدیو را ترجیح می دهید، در اینجا یک نسخه YouTube از این پست وجود دارد:

بررسی اجمالی

یک وب سایت ممکن است تنظیماتی را برای کنترل طرح رنگ به جای تکیه کامل بر اولویت سیستم ارائه دهد. این بدان معنی است که کاربران ممکن است در حالتی غیر از تنظیمات برگزیده سیستم خود مرور کنند. به عنوان مثال، سیستم کاربر در یک تم روشن است، اما کاربر ترجیح می دهد وب سایت در تم تاریک نمایش داده شود.

هنگام ساخت این ویژگی چندین ملاحظات مهندسی وب وجود دارد. به عنوان مثال، مرورگر باید در اسرع وقت از این اولویت آگاه شود تا از فلش رنگ صفحه جلوگیری شود، و کنترل باید ابتدا با سیستم همگام شود و سپس استثناهای ذخیره شده در سمت کلاینت را مجاز کند.

نمودار پیش نمایشی از بارگذاری صفحه جاوا اسکریپت و رویدادهای تعامل سند را نشان می دهد تا به طور کلی نشان دهد که 4 مسیر برای تنظیم موضوع وجود دارد.

نشانه گذاری

یک <button> باید برای جابجایی استفاده شود، زیرا پس از آن از رویدادها و ویژگی‌های تعاملی ارائه‌شده توسط مرورگر، مانند رویدادهای کلیک و قابلیت تمرکز بهره می‌برید.

دکمه

این دکمه به یک کلاس برای استفاده از CSS و یک شناسه برای استفاده از جاوا اسکریپت نیاز دارد. علاوه بر این، از آنجایی که محتوای دکمه به جای متن یک نماد است، یک ویژگی عنوان اضافه کنید تا اطلاعاتی در مورد هدف دکمه ارائه دهید. در آخر، یک [aria-label] برای نگه داشتن وضعیت دکمه نماد اضافه کنید، تا صفحه‌خوان‌ها بتوانند وضعیت تم را با افرادی که دارای اختلال بینایی هستند به اشتراک بگذارند.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label و aria-live مودب

برای اینکه به خوانندگان صفحه نمایش نشان دهید که تغییرات در aria-label باید اعلام شود، aria-live="polite" به دکمه اضافه کنید.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

این افزوده نشانه‌گذاری به صفحه‌خوان‌ها سیگنال می‌دهد که به جای aria-live="assertive" مودبانه به کاربر بگویند چه چیزی تغییر کرده است. در مورد این دکمه، بسته به اینکه aria-label به چه چیزی تبدیل شده است، "روشن" یا "تاریک" را اعلام می کند.

نماد گرافیک برداری مقیاس پذیر (SVG).

SVG راهی برای ایجاد اشکال با کیفیت بالا و مقیاس پذیر با حداقل نشانه گذاری فراهم می کند. تعامل با دکمه می تواند حالت های بصری جدیدی را برای بردارها ایجاد کند و SVG را برای آیکون ها عالی کند.

نشانه گذاری SVG زیر در داخل <button> قرار می گیرد:

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

aria-hidden به عنصر SVG اضافه شده است، بنابراین خوانندگان صفحه می‌دانند که آن را نادیده بگیرند زیرا به عنوان نمایشی علامت‌گذاری شده است. این کار برای تزئینات بصری، مانند نماد داخل یک دکمه، عالی است. علاوه بر ویژگی viewBox مورد نیاز در عنصر، ارتفاع و عرض را به دلایل مشابهی اضافه کنید که تصاویر باید اندازه های درون خطی داشته باشند .

خورشید

نماد خورشید نشان داده شده با پرتوهای خورشید محو شد و یک فلش صورتی داغ که به دایره در مرکز اشاره دارد.

گرافیک خورشید از یک دایره و خطوط تشکیل شده است که SVG به راحتی دارای اشکال است. <circle> با تنظیم ویژگی‌های cx و cy روی 12، که نیمی از اندازه دید (24) است، در مرکز قرار می‌گیرد و سپس شعاع ( r ) 6 داده می‌شود که اندازه را تعیین می‌کند.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

علاوه بر این، ویژگی mask به شناسه عنصر SVG اشاره می‌کند که در مرحله بعد ایجاد می‌کنید، و در نهایت رنگ پر شده‌ای به آن داده می‌شود که با رنگ متن صفحه با currentColor مطابقت داشته باشد.

پرتوهای خورشید

نماد خورشید نشان داده شده با مرکز خورشید محو شد و یک فلش صورتی داغ که به پرتوهای خورشید اشاره دارد.

سپس، خطوط پرتو خورشید درست در زیر دایره، در داخل یک گروه عنصر گروه <g> اضافه می شوند.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

این بار به جای اینکه مقدار fill currentColor باشد، stroke هر خط تنظیم می شود. خطوط به علاوه اشکال دایره ای با پرتوهای خورشیدی زیبا ایجاد می کنند.

ماه

برای ایجاد توهم انتقال یکپارچه بین نور (خورشید) و تاریکی (ماه)، ماه با استفاده از یک ماسک SVG، نماد خورشید را تقویت می کند.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
گرافیک با سه لایه عمودی برای کمک به نشان دادن نحوه عملکرد ماسک کردن. لایه بالایی یک مربع سفید با یک دایره سیاه است. لایه میانی نماد خورشید است. لایه پایین به عنوان نتیجه برچسب گذاری شده است و نماد خورشید را با یک برش نشان می دهد که در آن دایره سیاه لایه بالایی قرار دارد.

ماسک‌های دارای SVG قدرتمند هستند و به رنگ‌های سفید و سیاه اجازه می‌دهند بخش‌هایی از گرافیک دیگر را حذف یا شامل شوند. نماد خورشید توسط یک شکل <circle> ماه با یک ماسک SVG، به سادگی با حرکت دادن یک شکل دایره ای به داخل و خارج از یک ناحیه ماسک، گرفته می شود.

اگر CSS بارگیری نشود چه اتفاقی می افتد؟

تصویری از دکمه ساده مرورگر با نماد خورشید در داخل.

می تواند خوب باشد که SVG خود را آزمایش کنید، گویی CSS بارگیری نشده است تا مطمئن شوید که نتیجه فوق العاده بزرگ نیست یا باعث ایجاد مشکلاتی در چیدمان نمی شود. ویژگی های ارتفاع و عرض درونی در SVG به علاوه استفاده از currentColor قوانین سبک حداقلی را برای مرورگر ارائه می دهد تا در صورت بار نشدن CSS از آن استفاده کند. این باعث می شود که سبک های دفاعی خوبی در برابر آشفتگی شبکه ایجاد شود.

چیدمان

جزء سوئیچ تم دارای سطح کمی است، بنابراین برای چیدمان نیازی به شبکه یا فلکس باکس ندارید. در عوض، از موقعیت‌یابی SVG و تبدیل‌های CSS استفاده می‌شود.

سبک ها

سبک‌های .theme-toggle

عنصر <button> محفظه ای برای شکل ها و سبک های نماد است. این زمینه والد رنگ ها و اندازه های تطبیقی ​​را برای انتقال به SVG نگه می دارد.

اولین کار این است که دکمه را دایره ای کنید و سبک های دکمه پیش فرض را حذف کنید:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

در مرحله بعد، چند سبک تعامل اضافه کنید. یک سبک مکان نما برای کاربران ماوس اضافه کنید. افزودن touch-action: manipulation برای تجربه لمسی با واکنش سریع . حذف هایلایت نیمه شفاف iOS روی دکمه ها اعمال می شود. در آخر، به حالت فوکوس کمی فضای تنفسی از لبه عنصر بدهید:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

SVG داخل دکمه نیز به برخی سبک ها نیاز دارد. SVG باید متناسب با اندازه دکمه باشد و برای نرمی بصری، انتهای خط را گرد کند:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

اندازه تطبیقی ​​با جستجوی رسانه hover

اندازه دکمه آیکون کمی کوچک در 2rem است، که برای کاربران ماوس خوب است، اما می تواند برای یک اشاره گر درشت مانند یک انگشت مبارزه کند. با استفاده از جستجوی رسانه شناور برای تعیین افزایش اندازه، دکمه را مطابق با بسیاری از دستورالعمل‌های اندازه لمسی قرار دهید.

.theme-toggle {
  --size: 2rem;
  …
  
  @media (hover: none) {
    --size: 48px;
  }
}

سبک های SVG خورشید و ماه

دکمه جنبه های تعاملی جزء سوئیچ تم را نگه می دارد در حالی که SVG در داخل جنبه های بصری و متحرک را نگه می دارد. اینجاست که می توان نماد را زیبا کرد و زنده کرد.

تم سبک

ALT_TEXT_HERE

برای اینکه انیمیشن‌ها مقیاس و چرخش از مرکز اشکال SVG اتفاق بیفتند، transform-origin: center center . رنگ های تطبیقی ​​ارائه شده توسط دکمه در اینجا توسط اشکال استفاده می شود. ماه و خورشید از دکمه ارائه شده var(--icon-fill) و var(--icon-fill-hover) برای پر شدن خود استفاده می کنند، در حالی که پرتوهای خورشید از متغیرها برای stroke استفاده می کنند.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

تم تاریک

ALT_TEXT_HERE

سبک های ماه باید پرتوهای خورشید را حذف کنند، دایره خورشید را افزایش دهند و ماسک دایره را حرکت دهند.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
      }
    }
  }
}

توجه داشته باشید که تم تیره هیچ تغییر یا انتقال رنگی ندارد. مولفه دکمه والد مالک رنگ ها است، جایی که آنها در حال حاضر در یک زمینه تاریک و روشن سازگار هستند. اطلاعات انتقال باید پشت درخواست رسانه ترجیحی حرکت کاربر باشد.

انیمیشن

دکمه باید عملکردی و حالت دار باشد اما در این مرحله بدون انتقال باشد. بخش های زیر همه در مورد تعریف چگونگی و چه انتقال است.

به اشتراک گذاری درخواست های رسانه ای و واردات تسهیلات

برای سهولت قرار دادن انتقال ها و انیمیشن ها در پشت تنظیمات برگزیده حرکت سیستم عامل کاربر، افزونه PostCSS Custom Media استفاده از مشخصات CSS پیش نویس شده را برای نحو متغیرهای درخواست رسانه امکان پذیر می کند:

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

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

برای ساده‌سازی‌های CSS منحصربه‌فرد و آسان برای استفاده، بخش easings Open Props را وارد کنید:

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

خورشید

انتقال خورشید نسبت به ماه بازیگوش‌تر خواهد بود و این اثر را با کاهش‌های فنری به دست می‌آورد. پرتوهای خورشید باید در حین چرخش مقدار کمی تابیده شوند و مرکز خورشید در حین بزرگ شدن باید مقدار کمی تابیده شود.

سبک‌های پیش‌فرض (موضوع روشن) انتقال‌ها را تعریف می‌کنند و سبک‌های تم تیره، سفارشی‌سازی‌هایی را برای انتقال به روشن تعریف می‌کنند:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

در پانل انیمیشن در Chrome DevTools، می‌توانید یک جدول زمانی برای انتقال انیمیشن پیدا کنید. مدت زمان کل انیمیشن، عناصر و زمان بندی کاهش قابل بررسی است.

انتقال روشن به تاریکی
انتقال از تاریکی به روشنایی

ماه

موقعیت‌های روشن و تاریک ماه قبلاً تنظیم شده‌اند، سبک‌های انتقالی را در جست‌وجوی رسانه --motionOK اضافه کنید تا ضمن احترام به اولویت‌های حرکتی کاربر، آن را زنده کنید.

زمان بندی با تاخیر و مدت زمان در تمیز کردن این انتقال بسیار مهم است. برای مثال، اگر خورشید خیلی زود گرفته شود، انتقال به نظر هماهنگ یا بازیگوش نیست، احساس آشفتگی می کند.

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
انتقال روشن به تاریکی
انتقال از تاریکی به روشنایی

کاهش حرکت را ترجیح می دهد

در بیشتر چالش‌های رابط کاربری گرافیکی، من سعی می‌کنم برخی از انیمیشن‌ها را برای کاربرانی که حرکت کاهش‌یافته را ترجیح می‌دهند، نگه دارم. با این حال، این مؤلفه با تغییرات فوری حالت بهتر احساس می شود.

جاوا اسکریپت

کارهای زیادی برای جاوا اسکریپت در این مؤلفه وجود دارد، از مدیریت اطلاعات ARIA برای صفحه‌خوان‌ها تا دریافت و تنظیم مقادیر از حافظه محلی .

تجربه بارگذاری صفحه

مهم این بود که در بارگذاری صفحه هیچ چشمک زن رخ ندهد. اگر کاربری با طرح رنگ تیره نشان دهد که نور را با این مؤلفه ترجیح می دهد، سپس صفحه را مجدداً بارگیری می کند، ابتدا صفحه تاریک می شود و سپس به نور چشمک می زند. جلوگیری از این امر به معنای اجرای مقدار کمی از مسدود کردن جاوا اسکریپت با هدف تنظیم هرچه زودتر data-theme ویژگی HTML بود.

<script src="./theme-toggle.js"></script>

برای رسیدن به این هدف، ابتدا یک تگ <script> ساده در سند <head> قبل از هر نشانه گذاری CSS یا <body> بارگذاری می شود. هنگامی که مرورگر با یک اسکریپت بدون علامت مانند این روبرو می شود، کد را اجرا می کند و آن را قبل از بقیه HTML اجرا می کند. با استفاده کم از این لحظه مسدود کردن، می توان قبل از اینکه CSS اصلی صفحه را نقاشی کند، ویژگی HTML را تنظیم کرد، بنابراین از فلاش یا رنگ ها جلوگیری کرد.

جاوا اسکریپت ابتدا ترجیحات کاربر را در فضای ذخیره سازی محلی بررسی می کند و اگر چیزی در فضای ذخیره سازی یافت نشد، ترجیحات سیستم را بررسی می کند:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

تابعی برای تنظیم اولویت کاربر در حافظه محلی در ادامه تجزیه می شود:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

به دنبال آن یک تابع برای تغییر سند با تنظیمات برگزیده.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

نکته مهمی که در این مرحله باید به آن توجه کرد، وضعیت تجزیه اسناد HTML است. مرورگر هنوز از دکمه "#theme-toggle" اطلاعی ندارد، زیرا تگ <head> به طور کامل تجزیه نشده است. با این حال، مرورگر دارای یک document.firstElementChild ، با نام تگ <html> است. این تابع سعی می کند هر دو را برای همگام نگه داشتن آنها تنظیم کند، اما در اولین اجرا فقط می تواند تگ HTML را تنظیم کند. querySelector در ابتدا چیزی را پیدا نمی‌کند و اپراتور اختیاری زنجیره‌ای تضمین می‌کند که در صورت یافت نشدن و تلاش برای فراخوانی تابع setAttribute، خطای نحوی وجود نداشته باشد.

سپس، تابع reflectPreference() بلافاصله فراخوانی می‌شود تا سند HTML مجموعه ویژگی‌های data-theme خود را داشته باشد:

reflectPreference()

دکمه همچنان به ویژگی نیاز دارد، بنابراین منتظر رویداد بارگیری صفحه باشید، سپس پرس و جو، افزودن شنوندگان و تنظیم ویژگی‌ها در آن امن خواهد بود:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

تجربه جابجایی

هنگامی که دکمه کلیک می شود، موضوع باید در حافظه جاوا اسکریپت و در سند تعویض شود. ارزش موضوع فعلی باید بررسی شود و درباره وضعیت جدید آن تصمیم گیری شود. پس از تنظیم حالت جدید، آن را ذخیره کرده و سند را به روز کنید:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

همگام سازی با سیستم

منحصر به فرد این سوئیچ تم، همگام سازی با اولویت سیستم در حین تغییر است. اگر کاربر در حالی که صفحه و این مؤلفه قابل مشاهده است، تنظیمات برگزیده سیستم خود را تغییر دهد، سوئیچ تم برای مطابقت با اولویت کاربر جدید تغییر می‌کند، گویی که کاربر همزمان با سوئیچ موضوع، با سوئیچ تم تعامل داشته است.

این کار را با جاوا اسکریپت و رویداد matchMedia که به تغییرات یک درخواست رسانه گوش می دهد، برسید:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
تغییر اولویت سیستم MacOS وضعیت سوئیچ تم را تغییر می دهد

نتیجه

حالا که می دانید من چگونه این کار را انجام دادم، چگونه این کار را انجام می دهید‽🙂

بیایید رویکردهایمان را متنوع کنیم و همه راه‌های ساخت در وب را بیاموزیم. یک نسخه نمایشی ایجاد کنید، پیوندها را برای من توییت کنید ، و من آن را به بخش ریمیکس های انجمن در زیر اضافه می کنم!

ریمیکس های انجمن