React ile Özel Bir Toggle Anahtarı Nasıl Oluşturulur?
React uygulamalarınızda kullanıcı arayüzü (UI) bileşenleri tasarlarken, çoğu zaman standart HTML öğelerinin ötesine geçme ihtiyacı hissedersiniz. Özellikle etkileşimli ve görsel olarak çekici bileşenler söz konusu olduğunda, özel çözümler geliştirmek hem tasarım esnekliği hem de marka kimliği açısından büyük önem taşır. Bu makalede, React kullanarak sıfırdan, tamamen özelleştirilebilir, erişilebilir ve yeniden kullanılabilir bir toggle (açma/kapama) anahtarı bileşeni nasıl oluşturulacağını adım adım inceleyeceğiz.
Toggle anahtarları, bir ayarın iki durumu arasında (örneğin, açık/kapalı, evet/hayır) geçiş yapmak için kullanılan temel UI öğeleridir. Tarayıcıların varsayılan öğesi bu işlevi yerine getirse de, stil açısından sınırlıdır ve modern UI tasarımlarına genellikle uymaz. Bu nedenle, kendi özel toggle anahtarımızı oluşturarak hem görsel kontrolü ele alacak hem de erişilebilirlik standartlarını göz ardı etmeyeceğiz. Bu süreçte React’ın state yönetimi, prop’lar, olay dinleyicileri ve modern CSS tekniklerini bir araya getireceğiz.
Temel Bileşen Yapısı ve State Yönetimi
Özel toggle anahtarımızı oluşturmaya, React’ın temel bileşen yapısı ve state yönetimi ile başlayacağız. Bir toggle anahtarının en temel özelliği, içsel bir “açık” veya “kapalı” durumunu yönetmesidir. Bu durumu React’ın useState hook’u ile yöneteceğiz.
Toggle Bileşeni Oluşturma
İlk adım olarak, ToggleSwitch.js adında yeni bir dosya oluşturalım ve temel React functional component yapısını kuralım.
// ToggleSwitch.js
import React, { useState } from 'react';
import './ToggleSwitch.css'; // Stil dosyamızı import edeceğiz
const ToggleSwitch = ({ initialChecked = false, onChange, label }) => {
const [isChecked, setIsChecked] = useState(initialChecked);
const handleToggle = () => {
const newState = !isChecked;
setIsChecked(newState);
if (onChange) {
onChange(newState); // Dışarıya durumu bildir
}
};
return (
{label && {label}}
toggle-switch ${isChecked ? 'is-checked' : ''}}
onClick={handleToggle}
// Erişilebilirlik için gerekli ARIA ve klavye etkileşimleri burada eklenecek
>
);
};
export default ToggleSwitch;
Bu kod parçasında:
* useState(initialChecked): Bileşenin başlangıç durumunu initialChecked prop’u ile belirler ve isChecked adında bir state değişkeni ile bu durumu takip ederiz. setIsChecked fonksiyonu ile bu durumu güncelleriz.
* handleToggle: Anahtara tıklandığında çalışan bir fonksiyondur. Mevcut isChecked durumunu tersine çevirir, setIsChecked ile bileşenin dahili durumunu günceller ve eğer bir onChange prop’u tanımlanmışsa, bu prop’u yeni durumla birlikte çağırarak dışarıya bir bildirim gönderir. Bu, bileşenin ebeveynlerinin anahtarın durum değişikliklerinden haberdar olmasını sağlar.
* label: Toggle anahtarına eşlik eden bir metin etiketi sağlamak için kullanılır.
* toggle-switch-container: Etiket ve anahtarın kendisini saran bir kapsayıcıdır.
* toggle-switch: Anahtarın genel görünümünü temsil eden ana div öğesidir. is-checked sınıfı, anahtarın açık olduğu durumlarda stil değişiklikleri uygulamak için kullanılır.
* toggle-switch-thumb: Anahtarın içinde hareket eden “başparmak” veya “sürgü” kısmını temsil eder.
Bu temel yapı, toggle anahtarımızın davranışsal mantığını oluşturur. Ancak henüz görsel bir karşılığı yoktur.
Görsel Temsil ve JSX
Yukarıdaki JSX yapısı, toggle anahtarımızın görsel iskeletini oluşturur. toggle-switch sınıfına sahip div, anahtarın “yolunu” veya “izini” temsil ederken, toggle-switch-thumb sınıfına sahip iç div ise bu yol üzerinde hareket eden düğmeyi temsil eder. is-checked sınıfı ise anahtarın açık durumdayken farklı bir görünüme sahip olmasını sağlayacak kilit noktadır. Bu sınıfa bağlı olarak arka plan rengi ve sürgünün konumu değişecektir.
Bu basit JSX yapısı, CSS ile birleştiğinde görsel olarak tatmin edici bir toggle anahtarı oluşturmamızı sağlayacaktır.
Stil Oluşturma (CSS/SCSS)
Şimdi, ToggleSwitch.js dosyasında referans verdiğimiz ToggleSwitch.css dosyasını oluşturalım ve toggle anahtarımıza hayat veren stilleri ekleyelim. Bu bölümde, hem temel CSS kullanımı hem de SCSS gibi ön işlemcilerle daha düzenli ve sürdürülebilir stil yazma yaklaşımlarına değineceğiz.
Temel Stil Kurulumu
ToggleSwitch.css dosyamıza aşağıdaki temel stilleri ekleyelim:
/ ToggleSwitch.css /
.toggle-switch-container {
display: flex;
align-items: center;
gap: 10px; / Etiket ve anahtar arasına boşluk /
cursor: pointer; / Kapsayıcının tamamının tıklanabilir olduğunu belirtir /
user-select: none; / Metin seçimini engeller /
}
.toggle-switch-label {
font-family: sans-serif;
font-size: 1rem;
color: #333;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px; / Anahtarın genişliği /
height: 28px; / Anahtarın yüksekliği /
background-color: #ccc; / Kapalı durum arka plan rengi /
border-radius: 14px; / Yarıçapın yarısı ile tamamen yuvarlak kenarlar /
transition: background-color 0.2s ease-in-out; / Renk geçişi animasyonu /
flex-shrink: 0; / Kapsayıcı küçüldüğünde anahtarın boyutunu koru /
}
.toggle-switch-thumb {
position: absolute;
top: 2px; / Anahtarın içindeki sürgünün dikey konumu /
left: 2px; / Anahtarın içindeki sürgünün yatay konumu /
width: 24px; / Sürgünün genişliği /
height: 24px; / Sürgünün yüksekliği /
background-color: white; / Sürgünün rengi /
border-radius: 50%; / Tamamen yuvarlak bir sürgü /
transition: transform 0.2s ease-in-out; / Sürgü konumu geçişi animasyonu /
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); / Hafif bir gölge efekti /
}
Bu temel stiller, toggle anahtarımızın kapalı durumdaki görünümünü ve sürgünün başlangıç konumunu belirler. transition özellikleri, durum değişikliklerinde yumuşak animasyonlar sağlamak için kritik öneme sahiptir.
Açık/Kapalı Durumları İçin Stil
Şimdi, anahtar is-checked durumundayken nasıl görüneceğini tanımlayalım. Bu, React bileşenimizdeki koşullu sınıflandırma ile uyumlu olacaktır.
/ ToggleSwitch.css (devamı) /
.toggle-switch.is-checked {
background-color: #4CAF50; / Açık durum arka plan rengi (örneğin, yeşil) /
}
.toggle-switch.is-checked .toggle-switch-thumb {
transform: translateX(22px); / Sürgüyü sağa kaydır (genişlik - sürgü genişliği - 2padding) */
}
Burada, .toggle-switch.is-checked sınıfı, anahtar açık olduğunda arka plan rengini yeşile çevirir. .toggle-switch.is-checked .toggle-switch-thumb ise sürgüyü sağa doğru 22 piksel kaydırır. Bu değer, anahtarın toplam genişliği (50px) eksi sürgünün genişliği (24px) eksi her iki taraftan 2’şer piksel boşluk (toplam 4px) göz önünde bulundurularak hesaplanmıştır: 50 - 24 - (2*2) = 22px. Bu, sürgünün anahtarın içinde düzgün bir şekilde hareket etmesini sağlar.
SCSS Kullanımı (Opsiyonel ama iyi bir pratik)
Eğer projenizde SCSS (Sass) kullanıyorsanız, stillerinizi daha modüler ve okunabilir hale getirebilirsiniz. SCSS, iç içe yazım (nesting), değişkenler, mixin’ler ve fonksiyonlar gibi güçlü özellikler sunar.
Öncelikle, projenize Sass’ı kurmanız gerekebilir (npm install sass veya yarn add sass). Ardından, ToggleSwitch.css dosyasını ToggleSwitch.module.scss olarak yeniden adlandırabiliriz (CSS Modules ile birlikte kullanmak yaygın bir pratiktir).
/ ToggleSwitch.module.scss /
$toggle-width: 50px;
$toggle-height: 28px;
$thumb-size: 24px;
$thumb-offset: 2px; // İç boşluk
$border-radius: $toggle-height / 2;
$color-off: #ccc;
$color-on: #4CAF50;
$color-thumb: white;
$color-label: #333;
.toggle-switch-container {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.toggle-switch-label {
font-family: sans-serif;
font-size: 1rem;
color: $color-label;
}
.toggle-switch {
position: relative;
display: inline-block;
width: $toggle-width;
height: $toggle-height;
background-color: $color-off;
border-radius: $border-radius;
transition: background-color 0.2s ease-in-out;
flex-shrink: 0;
.toggle-switch-thumb {
position: absolute;
top: $thumb-offset;
left: $thumb-offset;
width: $thumb-size;
height: $thumb-size;
background-color: $color-thumb;
border-radius: 50%;
transition: transform 0.2s ease-in-out;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
&.is-checked {
background-color: $color-on;
.toggle-switch-thumb {
transform: translateX($toggle-width - $thumb-size - (2 * $thumb-offset));
}
}
}
Ve ToggleSwitch.js dosyasında stil importunu güncelleyin:
// ToggleSwitch.js
import React, { useState } from 'react';
import styles from './ToggleSwitch.module.scss'; // SCSS Modules importu
const ToggleSwitch = ({ initialChecked = false, onChange, label }) => {
const [isChecked, setIsChecked] = useState(initialChecked);
const handleToggle = () => {
const newState = !isChecked;
setIsChecked(newState);
if (onChange) {
onChange(newState);
}
};
return (
{label && {label}}
${styles['toggle-switch']} ${isChecked ? styles['is-checked'] : ''}}
onClick={handleToggle}
>
);
};
export default ToggleSwitch;
SCSS ile değişkenler kullanarak değerleri kolayca yönetebilir ve iç içe yazım ile daha okunabilir bir yapı elde edebilirsiniz. Ayrıca, CSS Modules kullanmak, stil çakışmalarını önleyerek bileşen tabanlı geliştirme için ideal bir çözüm sunar.
Erişilebilirlik (Accessibility – A11y)
Bir UI bileşeni tasarlarken erişilebilirlik, yalnızca belirli kullanıcı grupları için değil, herkes için daha iyi bir kullanıcı deneyimi sağlamanın temelidir. Görme engelliler için ekran okuyucular, motor engelliler için klavye navigasyonu gibi konuları ele alarak toggle anahtarımızı erişilebilir hale getirmeliyiz.
Neden Erişilebilirlik Önemli?
Web İçeriği Erişilebilirlik Yönergeleri (WCAG), web içeriğini daha erişilebilir hale getirmek için uluslararası bir standarttır. Bu standartlara uymak, yasal yükümlülüklerin yanı sıra, ürününüzün daha geniş bir kitleye ulaşmasını ve herkes için kullanılabilir olmasını sağlar. Özel bir toggle anahtarı oluştururken, varsayılan HTML öğesinin sağladığı yerleşik erişilebilirlik özelliklerini kaybederiz. Bu nedenle, bu özellikleri manuel olarak eklememiz gerekir.
ARIA Özellikleri
WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) özellikleri, HTML’in anlamsal yeteneklerini genişleterek, dinamik içerik ve özel UI bileşenlerinin erişilebilirliğini artırır. Toggle anahtarımız için aşağıdaki ARIA özelliklerini kullanacağız:
* role="switch": Bu, öğenin bir anahtar olduğunu ekran okuyuculara bildirir.
* aria-checked: Anahtarın mevcut durumunu (true/false) belirtir. Bu değer dinamik olarak isChecked state’imize bağlı olacaktır.
* aria-labelledby veya aria-label: Anahtarın amacını açıklayan bir etiket sağlar. Eğer görsel bir label prop’u kullanıyorsak, aria-labelledby ile bu etikete referans verebiliriz. Aksi takdirde, doğrudan aria-label kullanabiliriz.
Erişilebilirlik için en sağlam yaklaşımlardan biri, görsel olarak gizlenmiş ancak erişilebilirlik ağacında var olan bir öğesi kullanmaktır. Bu, tarayıcının yerleşik erişilebilirlik özelliklerinden (klavye navigasyonu, ARIA rolleri) faydalanmamızı sağlar ve manuel olarak birçok şeyi yönetme yükünü azaltır.
ToggleSwitch.js dosyamızı güncelleyelim:
// ToggleSwitch.js (Erişilebilirlik güncellemeleri)
import React, { useState, useRef, useId } from 'react'; // useId hook'unu ekledik
import styles from './ToggleSwitch.module.scss';
const ToggleSwitch = ({ initialChecked = false, onChange, label, disabled = false }) => {
const [isChecked, setIsChecked] = useState(initialChecked);
const inputRef = useRef(null);
const uniqueId = useId(); // Benzersiz ID oluşturmak için useId kullanıyoruz
const handleToggle = () => {
if (disabled) return; // Devre dışı ise işlem yapma
const newState = !isChecked;
setIsChecked(newState);
if (onChange) {
onChange(newState);
}
// Gizli input'un durumunu manuel olarak güncelle
if (inputRef.current) {
inputRef.current.checked = newState;
}
};
const handleKeyDown = (event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault(); // Varsayılan kaydırma/gönderme davranışını engelle
handleToggle();
}
};
return (
{label && (
)}
{}} // React'ın uyarı vermemesi için boş bir onChange
className={styles['visually-hidden']} // Görsel olarak gizle
disabled={disabled}
role="switch" // ARIA rolünü belirt
aria-checked={isChecked} // ARIA durumunu belirt
/>
${styles['toggle-switch']} ${isChecked ? styles['is-checked'] : ''} ${disabled ? styles['is-disabled'] : ''}}
// Görsel div'e focus yönetimi ve klavye etkileşimi atamıyoruz,
// çünkü gizli input bunları zaten hallediyor.
// Ancak, eğer gizli input kullanmasaydık, buraya tabIndex ve onKeyDown eklememiz gerekirdi.
>
);
};
export default ToggleSwitch;
Ve ToggleSwitch.module.scss‘ye visually-hidden sınıfını ekleyelim:
/ ToggleSwitch.module.scss (devamı) /
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.toggle-switch.is-disabled {
opacity: 0.6;
cursor: not-allowed;
}
.toggle-switch-container {
// ... mevcut stiller
&:has(input[disabled]) { // CSS :has() selector for disabled container
cursor: not-allowed;
}
}
Burada yapılan önemli değişiklikler:
* Gizli : Görsel olarak gizlenmiş bir checkbox ekledik. Bu, klavye navigasyonu (tab tuşu ile odaklanma) ve ekran okuyucu desteği gibi temel erişilebilirlik özelliklerini otomatik olarak sağlar.
* useId Hook’u: label öğesini gizli input ile ilişkilendirmek için benzersiz bir id sağlamak amacıyla React 18 ile gelen useId hook’unu kullandık. Bu, aynı bileşenin birden fazla örneği olduğunda ID çakışmalarını önler.
* htmlFor ve id: label öğesindeki htmlFor niteliği ile input öğesinin id‘sini eşleştirerek, etikete tıklandığında input’un tetiklenmesini sağlıyoruz.
* role="switch" ve aria-checked: Gizli input üzerinde ARIA özelliklerini tanımlayarak, ekran okuyuculara daha zengin anlamsal bilgi sağlıyoruz.
* disabled Prop’u: Bileşenin devre dışı bırakılabilmesi için bir disabled prop’u ekledik. Bu durumda, anahtar tıklamalara yanıt vermez ve görsel olarak grileşir. Gizli input’a da disabled niteliği ekleyerek erişilebilirlik ve davranışsal tutarlılığı sağlıyoruz.
* handleToggle Güncellemesi: handleToggle fonksiyonu artık disabled durumunu kontrol ediyor ve gizli input’un checked özelliğini manuel olarak güncelliyor.
Bu yaklaşım, özel bir UI bileşeni oluştururken en iyi erişilebilirlik uygulamalarından biridir, çünkü tarayıcının yerleşik erişilebilirlik motorundan en üst düzeyde faydalanırız.
Gelişmiş Özellikler ve Yeniden Kullanılabilirlik
Bileşenimizi daha esnek ve farklı senaryolarda yeniden kullanılabilir hale getirmek için bazı gelişmiş özellikler ekleyebiliriz.
Prop’lar ile Özelleştirme
Şimdiye kadar initialChecked, onChange, label ve disabled prop’larını gördük. Bunlara ek olarak, bileşenimizin daha da özelleştirilebilir olmasını sağlayacak başka prop’lar da düşünebiliriz:
* size: Anahtarın boyutunu kontrol etmek için (small, medium, large). Bu, CSS sınıfları veya CSS değişkenleri ile yönetilebilir.
* color: Anahtarın açık durumdaki rengini değiştirmek için. Bu da CSS değişkenleri veya inline stillerle yapılabilir.
Örnek olarak size prop’unu ekleyelim:
// ToggleSwitch.js (size prop'u eklendi)
import React, { useState, useRef, useId } from 'react';
import styles from './ToggleSwitch.module.scss';
const ToggleSwitch = ({
initialChecked = false,
onChange,
label,
disabled = false,
size = 'medium' // Yeni prop: 'small', 'medium', 'large'
}) => {
const [isChecked, setIsChecked] = useState(initialChecked);
const inputRef = useRef(null);
const uniqueId = useId();
const handleToggle = () => {
if (disabled) return;
const newState = !isChecked;
setIsChecked(newState);
if (onChange) {
onChange(newState);
}
if (inputRef.current) {
inputRef.current.checked = newState;
}
};
return (
${styles['toggle-switch-container']} ${styles[size-${size}]}} onClick={handleToggle}>
{label && (
)}
{}}
className={styles['visually-hidden']}
disabled={disabled}
role="switch"
aria-checked={isChecked}
/>
${styles['toggle-switch']} ${isChecked ? styles['is-checked'] : ''} ${disabled ? styles['is-disabled'] : ''}}
>
);
};
export default ToggleSwitch;
Ve ToggleSwitch.module.scss‘ye boyut sınıflarını ekleyelim:
/ ToggleSwitch.module.scss (devamı) /
// Boyut değişkenleri
$sizes: (
small: (width: 40px, height: 22px, thumb: 18px),
medium: (width: 50px, height: 28px, thumb: 24px),
large: (width: 60px, height: 34px, thumb: 30px)
);
@each $name, $values in $sizes {
.size-#{$name} {
.toggle-switch {
width: map-get($values, width);
height: map-get($values, height);
border-radius: map-get($values, height) / 2;
.toggle-switch-thumb {
width: map-get($values, thumb);
height: map-get($values, thumb);
top: (map-get($values, height) - map-get($values, thumb)) / 2;
left: (map-get($values, height) - map-get($values, thumb)) / 2;
}
&.is-checked .toggle-switch-thumb {
transform: translateX(map-get($values, width) - map-get($values, thumb) - (map-get($values, height) - map-get($values, thumb)));
}
}
}
}
Bu SCSS yapısı, @each döngüsü kullanarak farklı boyutlar için stilleri otomatik olarak oluşturur. Bu, tekrar eden CSS yazmaktan kaçınarak daha sürdürülebilir bir stil yönetimi sağlar.
Controlled vs. Uncontrolled Components
React’ta form elemanları genellikle iki kategoriye ayrılır:
* Uncontrolled Components (Kontrolsüz Bileşenler): Kendi iç state’lerini yönetirler. Bizim ToggleSwitch bileşenimiz, initialChecked prop’u ile başlangıç durumunu alıp kendi isChecked state’ini yönettiği için varsayılan olarak kontrolsüz bir bileşendir. Dışarıdan sadece onChange ile durum değişiklikleri hakkında bilgi verir.
* Controlled Components (Kontrollü Bileşenler): State yönetimi tamamen ebeveyn bileşen tarafından yapılır. checked gibi bir prop alırlar ve bu prop’un değeri, bileşenin mevcut durumunu belirler. Bileşen sadece onChange olayını tetikler, ancak kendi state’ini güncellemez; bu işi ebeveyn yapar.
Toggle anahtarımızı hem kontrollü hem de kontrolsüz olarak çalışabilecek şekilde tasarlamak, esnekliğini artırır. Bunu, checked prop’unun varlığına bağlı olarak iç state yönetimini değiştiren bir yaklaşımla yapabiliriz.
// ToggleSwitch.js (Controlled/Uncontrolled desteği)
import React, { useState, useRef, useId, useEffect } from 'react';
import styles from './ToggleSwitch.module.scss';
const ToggleSwitch = ({
checked, // Yeni prop: Kontrollü bileşen için
initialChecked = false,
onChange,
label,
disabled = false,
size = 'medium'
}) => {
// checked prop'u tanımlıysa kontrollü, değilse kontrolsüz
const isControlled = typeof checked === 'boolean';
const [internalChecked, setInternalChecked] = useState(isControlled ? checked : initialChecked);
const currentChecked = isControlled ? checked : internalChecked;
const inputRef = useRef(null);
const uniqueId = useId();
// checked prop'u değiştiğinde internalChecked'ı güncelle (sadece kontrollü bileşenlerde)
useEffect(() => {
if (isControlled) {
setInternalChecked(checked);
}
}, [checked, isControlled]);
const handleToggle = () => {
if (disabled) return;
const newState = !currentChecked;
if (!isControlled) {
setInternalChecked(newState); // Kontrolsüz ise kendi state'ini güncelle
}
if (onChange) {
onChange(newState); // Dışarıya durumu bildir (her zaman)
}
// Gizli input'un durumunu manuel olarak güncelle
if (inputRef.current) {
inputRef.current.checked = newState;
}
};
return (
${styles['toggle-switch-container']} ${styles[size-${size}]}} onClick={handleToggle}>
{label && (
)}
{}} // React'ın uyarı vermemesi için boş bir onChange
className={styles['visually-hidden']}
disabled={disabled}
role="switch"
aria-checked={currentChecked}
/>
${styles['toggle-switch']} ${currentChecked ? styles['is-checked'] : ''} ${disabled ? styles['is-disabled'] : ''}}
>
);
};
export default ToggleSwitch;
Bu güncelleme ile:
* isControlled değişkeni, checked prop’unun varlığını kontrol eder.
* internalChecked state’i, bileşenin kontrolsüz moddayken kendi durumunu yönetmesini sağlar.
* currentChecked değişkeni, bileşenin mevcut durumunu her zaman doğru bir şekilde yansıtır (kontrollü ise checked prop’undan, kontrolsüz ise internalChecked state’inden).
* useEffect hook’u, checked prop’u değiştiğinde (yani bileşen kontrollü olduğunda) internalChecked state’ini günceller.
* handleToggle fonksiyonu, isControlled durumuna göre internalChecked‘ı güncelleyip güncellememe kararı verir.
Bu sayede, ToggleSwitch bileşenini aşağıdaki gibi kullanabilirsiniz:
// App.js (Kontrolsüz kullanım)
import React, { useState } from 'react';
import ToggleSwitch from './ToggleSwitch';
function App() {
const handleToggleChange = (newState) => {
console.log('Toggle durumu değişti (kontrolsüz):', newState);
};
return (
);
}
export default App;
// App.js (Kontrollü kullanım)
import React, { useState } from 'react';
import ToggleSwitch from './ToggleSwitch';
function App() {
const [isDarkMode, setIsDarkMode] = useState(false);
const handleDarkModeToggle = (newState) => {
setIsDarkMode(newState);
console.log('Karanlık Mod durumu:', newState);
};
return (
Karanlık mod şu anda: {isDarkMode ? 'AÇIK' : 'KAPALI'}
);
}
export default App;
Forwarding Ref’ler (Opsiyonel)
Nadiren de olsa, ebeveyn bir bileşenin custom toggle anahtarımızın altındaki gizli input öğesine doğrudan referans erişimi (ref) sağlaması gerekebilir (örneğin, bir form gönderimi öncesi odaklanma veya doğrulama için). Bu senaryoda React.forwardRef kullanabiliriz.
// ToggleSwitch.js (forwardRef eklendi)
import React, { useState, useRef, useId, useEffect, forwardRef, useImperativeHandle } from 'react';
import styles from './ToggleSwitch.module.scss';
const ToggleSwitch = forwardRef(({
checked,
initialChecked = false,
onChange,
label,
disabled = false,
size = 'medium'
}, ref) => { // ref prop'u buraya gelir
const isControlled = typeof checked === 'boolean';
const [internalChecked, setInternalChecked] = useState(isControlled ? checked : initialChecked);
const currentChecked = isControlled ? checked : internalChecked;
const inputRef = useRef(null);
const uniqueId = useId();
useEffect(() => {
if (isControlled) {
setInternalChecked(checked);
}
}, [checked, isControlled]);
const handleToggle = () => {
if (disabled) return;
const newState = !currentChecked;
if (!isControlled) {
setInternalChecked(newState);
}
if (onChange) {
onChange(newState);
}
if (inputRef.current) {
inputRef.current.checked = newState;
}
};
// Ebeveyn bileşenin bu referans üzerinden erişebileceği yöntemleri tanımla
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
// İsterseniz başka yöntemler de ekleyebilirsiniz, örneğin toggle()
toggle: handleToggle
}));
return (
${styles['toggle-switch-container']} ${styles[size-${size}]}} onClick={handleToggle}>
{label && (
)}
{}}
className={styles['visually-hidden']}
disabled={disabled}
role="switch"
aria-checked={currentChecked}
/>
${styles['toggle-switch']} ${currentChecked ? styles['is-checked'] : ''} ${disabled ? styles['is-disabled'] : ''}}
>
);
});
export default ToggleSwitch;
Ve App.js‘de kullanımı:
// App.js (forwardRef kullanımı)
import React, { useRef } from 'react';
import ToggleSwitch from './ToggleSwitch';
function App() {
const toggleRef = useRef(null);
const handleFocusToggle = () => {
toggleRef.current?.focus(); // ToggleSwitch'in içindeki input'a odaklan
};
return (
);
}
export default App;
useImperativeHandle hook’u ile, ebeveyn bileşenlerin ref üzerinden erişebileceği belirli fonksiyonları (örneğin focus()) tanımlayabiliriz. Bu, bileşenin iç detaylarını ifşa etmeden kontrollü bir arayüz sağlar.
Tam Kod Örneği
Yukarıdaki tüm bölümleri bir araya getiren nihai kod örnekleri:
// ToggleSwitch.js
import React, { useState, useRef, useId, useEffect, forwardRef, useImperativeHandle } from 'react';
import styles from './ToggleSwitch.module.scss';
const ToggleSwitch = forwardRef(({
checked,
initialChecked = false,
onChange,
label,
disabled = false,
size = 'medium'
}, ref) => {
const isControlled = typeof checked === 'boolean';
const [internalChecked, setInternalChecked] = useState(isControlled ? checked : initialChecked);
const currentChecked = isControlled ? checked : internalChecked;
const inputRef = useRef(null);
const uniqueId = useId();
useEffect(() => {
if (isControlled) {
setInternalChecked(checked);
}
}, [checked, isControlled]);
const handleToggle = () => {
if (disabled) return;
const newState = !currentChecked;
if (!isControlled) {
setInternalChecked(newState);
}
if (onChange) {
onChange(newState);
}
if (inputRef.current) {
inputRef.current.checked = newState;
}
};
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
toggle: handleToggle
}));
return (
${styles['toggle-switch-container']} ${styles[size-${size}]}} onClick={handleToggle}>
{label && (
)}
{}}
className={styles['visually-hidden']}
disabled={disabled}
role="switch"
aria-checked={currentChecked}
/>
${styles['toggle-switch']} ${currentChecked ? styles['is-checked'] : ''} ${disabled ? styles['is-disabled'] : ''}}
>
);
});
export default ToggleSwitch;
// ToggleSwitch.module.scss
$color-off: #ccc;
$color-on: #4CAF50;
$color-thumb: white;
$color-label: #333;
.toggle-switch-container {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
&:has(input[disabled]) {
cursor: not-allowed;
}
}
.toggle-switch-label {
font-family: sans-serif;
font-size: 1rem;
color: $color-label;
}
.toggle-switch {
position: relative;
display: inline-block;
background-color: $color-off;
transition: background-color 0.2s ease-in-out;
flex-shrink: 0;
box-sizing: border-box; // Padding ve border'ın genişliğe dahil olmasını sağlar
.toggle-switch-thumb {
position: absolute;
background-color: $color-thumb;
border-radius: 50%;
transition: transform 0.2s ease-in-out;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
&.is-checked {
background-color: $color-on;
}
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
// Boyut değişkenleri ve mixin
$sizes: (
small: (width: 40px, height: 22px, thumb: 18px),
medium: (width: 50px, height: 28px, thumb: 24px),
large: (width: 60px, height: 34px, thumb: 30px)
);
@each $name, $values in $sizes {
.size-#{$name} {
.toggle-switch {
width: map-get($values, width);
height: map-get($values, height);
border-radius: map-get($values, height) / 2;
$thumb-offset: (map-get($values, height) - map-get($values, thumb)) / 2; // Dinamik offset
.toggle-switch-thumb {
width: map-get($values, thumb);
height: map-get($values, thumb);
top: $thumb-offset;
left: $thumb-offset;
}
&.is-checked .toggle-switch-thumb {
transform: translateX(map-get($values, width) - map-get($values, thumb) - (2 * $thumb-offset));
}
}
}
}
// App.js (Kullanım örneği)
import React, { useState, useRef } from 'react';
import ToggleSwitch from './ToggleSwitch';
import './App.css'; // Global stilleriniz için
function App() {
const [isNotificationsEnabled, setIsNotificationsEnabled] = useState(true);
const [isDarkMode, setIsDarkMode] = useState(false);
const toggleRef = useRef(null);
const handleFocusToggle = () => {
toggleRef.current?.focus();
};
return (
Kontrolsüz Kullanım
Bildirimler: {isNotificationsEnabled ? 'Açık' : 'Kapalı'}
Kontrollü Kullanım
Karanlık Mod: {isDarkMode ? 'AÇIK' : 'KAPALI'}
Boyut ve Devre Dışı Bırakma
Ref Kullanımı
);
}
export default App;
Sonuç
Bu makalede, React kullanarak sıfırdan, tamamen özelleştirilebilir ve erişilebilir bir toggle anahtarı bileşeni oluşturmanın tüm adımlarını detaylı bir şekilde inceledik. Temel state yönetiminden başlayarak, modern CSS (veya SCSS) ile görselleştirme, gizli bir kullanarak erişilebilirliği sağlama, kontrollü/kontrolsüz bileşen davranışını destekleme ve forwardRef ile daha ileri düzey ref yönetimi gibi konulara değindik.
Kendi özel UI bileşenlerinizi oluşturmak, React geliştiricisi olarak size büyük bir esneklik ve kontrol sağlar. Bu sadece bir toggle anahtarı için değil, herhangi bir karmaşık UI öğesi için geçerlidir. Erişilebilirlik ve yeniden kullanılabilirlik ilkelerini baştan sona uygulamak, daha sağlam, kapsayıcı ve bakımı kolay uygulamalar geliştirmenizi sağlar. Bu bileşeni kendi React projelerinize entegre edebilir, tasarım sistemlerinize uygun hale getirebilir ve uygulamanızın genel kullanıcı deneyimini zenginleştirebilirsiniz.