Componentspecifieke lichte/donkere thema's

Webfuncties: color-scheme light-dark()

Hoewel de eigenschap color-scheme vaker wordt ingesteld op de root, kan deze ook worden ingesteld op afzonderlijke elementen om ze een ander kleurenschema te geven dan de rest van de pagina. Dit kan handig zijn voor componenten die altijd in een specifiek kleurenschema moeten worden bekeken (bijvoorbeeld altijd in de donkere of lichte modus).

Voorbeelden van use cases zijn:

  • Elementen die om esthetische redenen vaak in de donkere modus staan, zelfs op pagina's in de lichte modus, zoals codeblokken, mediaspelers en fotogalerijen
  • Gebieden met media die zijn ontworpen voor een lichte achtergrond (bijvoorbeeld afbeeldingen, video's, illustraties, afdrukvoorbeelden) kunnen worden ingesteld op de lichte modus, zelfs als de rest van de pagina in de donkere modus staat.
  • Elementen waarvan het kleurenschema wordt beheerd door een instelling op gebruikersniveau, zoals componentvoorbeelden
  • Insluitingen die niet zowel de lichte als de donkere modus ondersteunen
  • Ontwerptools, kaarten, visualisaties, games, enzovoort.

Wanneer moet je kleuren wijzigen en wanneer moet je color-scheme afdwingen?

Niet elk element dat lichtere tekst op een donkerdere achtergrond gebruikt in de lichte modus of donkerdere tekst op een lichtere achtergrond in de donkere modus, heeft een andere color-scheme nodig. Een primaire knop kan bijvoorbeeld blauw worden weergegeven met witte tekst in de lichte modus, maar dat rechtvaardigt geen color-scheme: dark.

Als vuistregel geldt dat elementen die een andere color-scheme gebruiken, meestal complexe oppervlakken zijn die hun eigen visuele context bepalen, in plaats van eenvoudige ondiepe containers.

Als je overweegt een andere color-scheme te gebruiken voor een element, stel jezelf dan de volgende vragen:

  • Moet de ingebouwde browser-UI die niet op een andere manier is aangepast (bijvoorbeeld formulierbediening, scrollbalken, enzovoort) dat kleurenschema gebruiken of zich aanpassen aan het kleurenschema van de pagina? -> if the former, don't use color-scheme.
  • Moeten light-dark()-kleuren worden opgelost zoals voor de rest van de pagina of op basis van de overschrijving? -> if the former, don't use color-scheme.
  • Moeten afstammelingen in die color-scheme staan? Als dat niet het geval is, gebruik je color-scheme niet.

Basisimplementatie

Componentspecifieke overschrijvingen worden meestal (maar niet noodzakelijkerwijs) gebruikt op pagina's die ook meerdere kleurenschema's ondersteunen via een algemene color-scheme. Ga naar Donkere modus voor meer informatie over hoe je de donkere modus voor de hele pagina implementeert.

Zodra er een paginabrede color-scheme is ingesteld en je kleurtokens gebruikt die daar gevoelig voor zijn (bijvoorbeeld via light-dark()), kun je color-scheme instellen voor specifieke componenten om de kleurmodus voor die substructuur te overschrijven:

pre, code, .dark {
  color-scheme: dark;
}

Sommige browsers passen componenten automatisch aan een ander kleurenschema aan. Als je het aangegeven kleurenschema in alle gevallen wilt afdwingen, gebruik je only, dus color-scheme: only dark; in plaats van color-scheme: dark;.

Niet-kleurwaarden aanpassen

light-dark() werkt op dit moment alleen voor kleuren.

Praktische tips

  • VERPLICHT: Stel color-scheme niet in voor elementen zonder achtergrond, omdat je dan het risico loopt dat achtergrond- en tekstkleurparen uit verschillende kleurenschema's worden gemengd, waardoor de tekst onleesbaar wordt.
  • OPTIONEEL: Hoewel het makkelijker is om dezelfde kleurparen te hergebruiken als de paginabrede donkere modus, kunnen we verschillende kleurparen definiëren voor deze componenten. We willen bijvoorbeeld dat een component in de donkere modus die wordt gebruikt op een pagina in de lichte modus, iets minder donker is dan wanneer dezelfde component in de donkere modus wordt gebruikt op een pagina die helemaal in de donkere modus staat.

Bekende problemen om rekening mee te houden

Belangrijk aandachtspunt: overname van light-dark()-kleuren

light-dark() wordt omgezet op het moment van de berekende waarde. Dit betekent dat overgenomen <color>-eigenschappen die zijn ingesteld op een light-dark()-kleur, maar één van de twee kleuren doorgeven aan hun afstammelingen, niet de light-dark()-expressie zelf.

Bijvoorbeeld:

  • Ingebouwde kleureigenschappen die worden overgenomen, zoals color, accent-color, fill, stroke, text-shadow, caret-color
  • Geregistreerde overerfbare aangepaste eigenschappen met syntax: <color> en inherits: true
  • Een andere <color>-property ingesteld op inherit

Dit betekent dat je het volgende moet doen:

  • Registreer aangepaste eigenschappen die bedoeld zijn voor designtokens (bijv. --surface-color) NIET als <color>. Tokens moeten hun light-dark()-expressie live houden, zodat afstammelingen ze opnieuw kunnen oplossen onder een andere color-scheme.
  • Als je color-scheme instelt voor een element, geef je alle overgenomen <color>-eigenschappen opnieuw op die misschien zijn ingesteld op light-dark()-waarden (rechtstreeks of via ontwerptokens), zelfs als dat dezelfde ontwerptoken is.
  • Gebruik inherit NIET voor <color>-property's voor elementen met een color-scheme-overschrijving (je kunt dit wel gebruiken voor de onderliggende elementen).
  • Gebruik WEL geregistreerde <color>-property's voor de tegenovergestelde use case: als je bewust een momentopname wilt maken van de opgeloste kleur van de bovenliggende property en wilt voorkomen dat deze opnieuw wordt opgelost onder de color-scheme van de onderliggende property. Bijvoorbeeld om de pagina-achtergrond vast te leggen en ergens anders te gebruiken.
  • Als je een kleur wilt animeren, gebruik je een afzonderlijke @property-geregistreerde <color>-property voor het element dat wordt geanimeerd (registratie is vereist voor kleurinterpolatie). Dit is geen designtoken, maar een animatiedoel per element, dus dit is niet in strijd met de bovenstaande regel.

Voorbeeld:

:root {
    --accent-color: light-dark(blue, skyblue);
    --surface-color: light-dark(white, #222);
    --text-color: light-dark(#111, white);

    color-scheme: light dark;
    accent-color: var(--accent-color);
    color: var(--text-color);
}

body {
    /* --surface-color dynamically switches despite being inherited because --surface-color is not registered */
    background: var(--surface-color);
}

pre, code {
    color-scheme: dark;
    background: var(--surface-color);

    /* Without this, accent-color would be blue, not skyblue! */
    accent-color: var(--accent-color);

    /* Without this, text-color would be #111, not white! */
    color: var(--text-color);
}

Problemen om rekening mee te houden als je color-scheme gebruikt

  • Chrome en Firefox respecteren color-scheme voor iframes: ze geven ingesloten pagina's weer in het juiste kleurenschema en passen de mediaquery prefers-color-scheme van de ingesloten pagina aan de color-scheme van de insluitingscontext aan. Safari doet dit niet en herleidt prefers-color-scheme naar de systeeminstelling, zelfs binnen iframes.
    • Als je zowel de bovenliggende pagina als het iframe beheert: Geef het kleurenschema van de bovenliggende pagina expliciet door aan het iframe, via een URL-parameter (?theme=dark) op het moment dat het iframe wordt gemaakt of via postMessage() (waarmee je ook kunt reageren op wijzigingen tijdens de runtime). Stel in het iframe een klasse in op <html> (en/of color-scheme op :root) op basis van dat signaal in plaats van te vertrouwen op prefers-color-scheme.
    • Als je alleen de ingesloten pagina beheert: Er is geen betrouwbare manier om de color-scheme van de insluitingscontext te detecteren vanuit het iframe in Safari. Maak een expliciete themaparameter beschikbaar in je insluit-API (bijvoorbeeld een querytekenreeks of postMessage-protocol) en documenteer deze voor insluiters.

Reservestrategieën

Fallbacks en browserondersteuning voor kleurenschema

De property color-scheme is progressive enhancement. Browsers die dit niet ondersteunen, negeren deze property en gebruiken de standaard UI voor de lichte modus.

Als je je wilt aanpassen aan de voorkeuren van de gebruiker in oudere browsers, gebruik je prefers-color-scheme-mediaquery's om verschillende kleuren te bieden als de donkere modus de voorkeur heeft.

  • Gebruik de mediaquery om aangepaste eigenschappen aan te zetten op :root of html.
  • Gebruik de mediaquery niet voor afzonderlijke componenten, tenzij de component een heel specifiek type aanpassing voor de donkere modus vereist dat verder gaat dan kleuren.
:root {
  /* Define brand colors for each mode */
  --color-brand-light: #0056b3;
  --color-brand-dark: #00e5ff;
  --color-brand: var(--color-brand-light);

  /* MANDATORY: Fallback for browsers without light-dark support */
  @media (prefers-color-scheme: dark) {
    --color-brand: var(--color-brand-dark);
  }

  /* Ignored in older browsers */
  color-scheme: light dark;
}

button.primary {
    background-color: var(--color-brand);
}

Fallbacks en browserondersteuning voor light-dark()

Voor browsers die color-scheme ondersteunen maar nog niet light-dark(), moeten lichte en donkere versies van kleuren eerst worden gedefinieerd als aangepaste eigenschappen. De mediaquery prefers-color-scheme moet worden gebruikt om kleuren in te stellen voor de betreffende modus, zoals in het voorbeeld hieronder:

:root {
  /* Define browser UI accent color for each mode */
  --brand-accent-light: #0056b3;
  --brand-accent-dark: #00e5ff;
  --accent-color: var(--brand-accent-light);

  /* MANDATORY: Fallback for browsers without light-dark support */
  @media (prefers-color-scheme: dark) {
    --accent-color: var(--brand-accent-dark);
  }

  /* OPTIONAL: use light-dark() for more control of built-in UI colors */
  @supports (color: light-dark(white, black)) {
    --accent-color: light-dark(var(--brand-accent-light), var(--brand-accent-dark));
  }

  /* MANDATORY: Automatically adapt native UI to user system preferences */
  color-scheme: light dark;

  /* Example inherited color property */
  accent-color: var(--accent-color);
}

pre, code {
  color-scheme: dark;

  /* **Mandatory**: any inherited color properties must be set again, even if to the same design tokens */
  accent-color: var(--accent-color);
}