Ein kleines CSS-Rätsel (mit Auflösung)

Veröffentlicht am 9. November 2015

Am Freitag twitterte ich ein kleines CSS-Rätsel und bekam so viele verschiedene Antworten, dass ich an dieser Stelle nochmal die ausführliche (richtige) Antwort aufschreiben möchte.

Die Frage dreht sich darum, welche Farbe der Text im folgenden HTML-Schnipsel erhält …

<p class="foo">
  A
</p>

… wenn dieses CSS auf ihn losgelassen wird:

p.foo:not(#baz) {
  color: yellow;
}

p.foo:not(.bar) {
  color: blue;
}

p.foo:not(p#foo) {
  color: magenta;
}

p.foo {
  color: green;
}

.foo {
  color: red;
}

Die meisten Quiz-Teilnehmer tippten auf Magenta und waren erstaunt, in ihren Browsern Gelb zu sehen. Einige wenige tippten auch auf Magenta und sahen es am Ende auch in ihrem Browser. Wie so oft in der Frontend-Entwicklung lautet die richtige Antwort auf dieses Rätsel: es kommt darauf an! Je nachdem welchen Browser man auf den Code loslässt, kommt das eine oder das andere heraus.

Eigentlicher Gegenstand des Rätsels ist die Selektorspezifität. Wären alle Selektoren gleich viel wert, würde die letzte auf ein Element anwendbare CSS-Regel alle anderen überstimmen und der Text würde rot. Doch verschiedene Selektoren haben verschieden viel Gewicht: ID-Selektoren schlagen Klassen-Selektoren, Klassen-Selektoren schlagen Element-Selektoren und kombinierte Selektoren wie z.B. p.foo wiegen einen aus ihren Bestandteilen errechneten Wert (Cheat Sheet). Da unter allen Selektoren im Rätsel der für die rote Farbe der unspezifischste ist, muss der Text eine andere Farbe bekommen. Aber welche?

Die Spezifität eines Selektors setzt sich aus drei Bestandteilen zusammen:

  • A ist die Anzahl aller ID-Selektoren im Selektor
  • B ist die Anzahl aller Klassen-, Attribut- und Pseudoklassen-Selektoren
  • C ist die Anzahl aller Typ- und Pseudoelement-Selektoren

Die :not()-Pseudoklasse selbst trägt nicht zur Spezifität bei, der in ihr enthaltene Selektor hingegen schon. Der Universal-Selektor * wird ignoriert.

Um zu entscheiden ob ein Selektor spezifischer ist als ein anderer werden die drei Komponenten A, B und C verglichen. Der Selektor A mit dem größeren A-Wert ist spezifischer; bei Gleichstand wird B verglichen und herrscht auch hier Gleichstand, wird C verglichen. Bei kompletter Gleichwertigkeit siegt der zuletzt definierte Selektor.

Berechnen wir doch mal (mit z.B. diesem Tool) die Spezifität aller Selektoren im Rätsel und sortieren sie entsprechend ihres Gewichts:

Selektor Spezifität Farbe
p.foo:not(p#foo) (1, 1, 2) Magenta
p.foo:not(#baz) (1, 1, 1) Gelb
p.foo:not(.bar) (0, 2, 1) Blau
p.foo (0, 1, 1) Grün
.foo (0, 1, 0) Rot

Eigentlich klarer Sieg für das Team Magenta, das dank des p#foo in :not() einen Typselektor mehr hat als Gelb. Warum aber erkennt nicht jeder Browser Magenta als Sieger an? Ganz einfach: :not(p#foo) ist erst ab Selectors Level 4 zulässig! In CSS3 kann :not() nur einfache Selektoren (simple selectors, d.h. alleinstehende Typ-, Attribut-, Klassen-, ID-, oder Pseudoklassen-Seletoren) aufnehmen. Der Selektor p#foo ist ein aus zwei Teilen zusammengesetzter compound selector und im Kontext der :not()-Pseudoklasse für alle außer den allermodernsten Browsern unverständlich.

Interessantes Detail am Rande: Selectors Level 4 definiert zwei Selektor-Profile, ein schnelles und ein vollständiges. Wie man erahnen kann, enthält das schnelle Selektor-Profil nicht alle Features des vollständigen und soll vor allem direkt im Browser-Rendering zum Einsatz kommen. Das vollständige Feature-Set ist für die diversen DOM-Selektor-APIs vorgesehen. In beiden Profilen kann die :not()-Pseudoklasse compound selectors verwenden d.h. dieses neue Feature wird universell einsetzbar sein. Nur der Einsatz von komplexen Selektoren, d.h. solchen mit Kombinatoren, ist dem vollständigen Profil vorbehalten.

Warum man die Finger von den Prototypen nativer Objekte zu lassen hat

Veröffentlicht am 10. August 2015

In regelmäßigen Abständen schlägt bei mir die Frage auf, ob es nicht in ganz bestimmten Fällen doch in Ordnung wäre, die Prototypen der nativen JavaScript- und DOM-Objekte zu erweitern. Meine Antwort darauf ist immer: in öffentlich zugänglichen Projekten, ist das ein absolutes No-Go. Gegen den Einsatz bei privaten Projekten oder bei Spezialtools wie Testframeworks (z.B. should.js) lassen sich auch Argumente finden, aber darüber kann man durchaus zweierlei Meinung sein.

Wenn es darum geht, Gründe gegen das Erweitern der nativen Built-Ins zu finden, wird oft die Kollisionsgefahr zuerst herausgekramt. Würde man in seinem Code Element.prototype.foo definieren, könnte eine Third-Party-Library jetzt oder in Zukunft auch etwas unter Element.prototype.foo implementieren wollen. Diesem Argument lässt sich entgegensetzen, dass das erstens nie passiert und sich zweitens in 99% aller Fälle problemlos reparieren lassen dürfte – einfach das eigene Element.prototype.foo in Element.prototype.foobar umbenennen und schon funktioniert wieder alles. Aber wenn man vom Start weg nicht den Namen Element.prototype.foo, sondern etwas wie Element.prototype.$_foo wählt, werden derartige Reparaturen höchstwahrscheinlich gar nicht erst nötig werden. Auf Projektebene ist die Kollisionsgefahr mit anderen Libraries praktisch nicht gegeben.

Das größere Problem mit dem Erweitern von nativen Objekten besteht nicht auf der Projekt-, sondern auf der Web-Ebene. Browser und Spezifikationen müssen bei der Entwicklung neuer Features immer darauf achten, dass die Neuheiten kompatibel zum gesamten existierenden, öffentlichen HTML/JavaScript/CSS-Code sind. Sie haben es also mit der denkbar größten Legacy-Codebase der Welt zu tun, nämlich dem kompletten WWW. Das macht die Entwicklung von Neuheiten recht knifflig, nicht zuletzt weil einige Entwickler nicht die Finger von den Prototypen nativer Elemente lassen konnten. Das beste Beispiel dafür ist die in ECMAScript 6 neu eingeführte Funktion Array.prototype.includes(), die prüft ob ein Array ein gegebenes Element enthält. Ursprünglich sollte das neue Feature den Namen Array.prototype.contains() tragen, doch es stellte sich heraus, dass mehrere Webseiten bereits länger eine genau so benannte, aber subtil anders implementierte Funktion gleichen Namens verwenden. Diese Funktion stammt aus der JavaScript-Library Mootools, die schon vor Jahren den Zenit ihrer Popularität überschritten hatte. Aber da es immer noch Webseiten gibt, die Mootools in der einen oder anderen Version verwenden, war der Name Array.prototype.contains() für ECMAScript 6 nicht zu gebrauchen. Es erfolgte die Umbenennung in includes() und in Folge musste natürlich auch die entsprechende String-Methode für ES6 umbenannt werden.

Das Beispiel zeigt, was passieren kann, wenn man als normaler Webentwickler die Prototypen von nativen Built-Ins erweitert: man fuhrwerkt im Legacy-Code jeder Browserengine und jeder Spezifikation herum, die es jemals gab und jemals geben wird! Und da man nicht weiß, in welche Richtung die Entwicklung der Webstandards in 3 Jahren gehen wird, sollte man tunlichst die Finger von den entsprechenden Objekten lassen. Vielleicht wird es nie ein Problem sein, sein eigenes Element.prototype.$_foo zu definieren. Vielleicht wird man genau damit aber auch ein Feature von DOM 6.0 oder ECMAScript 12 blockieren.

Zusammengefasst ist es in Ordnung, die nativen Prototypen zu erweitern, wenn entweder

  1. das Projekt privat ist (z.B. Intranet-App, Testing-Tool) und die entsprechenden Codezeilen auch niemals das WWW betreten werden, und sei es durch Copy & Paste in ein neues Projekt
  2. oder man sicher weiß, dass der entsprechende Code niemals mit irgendeinem zukünftigen Standard kollidieren wird oder dass zu dem Zeitpunkt, an dem die Kollision geschehen wird, garantiert vom fraglichen Code keine einzige Zeile mehr irgendwo im WWW aufzufinden ist.

Ich weiß nicht, wie man diese Bedingungen erfüllen will, ohne Hellseher zu sein. Daher lasse ich die Finger von den Prototypen der nativen Built-Ins und empfehle das auch jedem, der mich fragt.

Fragen zu HTML5 und Co beantwortet 21 - Semantische Elemente, Source Maps, das a-Element, Whitespace

Veröffentlicht am 13. Juli 2015

Die nebenstehende Fragen-Artikel-Box wird so langsam zu lang. Für das nächste Redesign ist eine Einklapp-Funktionalität fest eingeplant, aber bis dahin müssen wir das Layout noch so ertragen wie es ist. Falls ihr euren Teil zur Verlängerung der Box betragen wollt, schickt auch ihr mir einfach eure Fragen zu HTML5 und Co.

HTML5-Elemente nicht benutzen – ein Fehler?

Ich habe noch nie wirklich HTML5-Elemente oder -Attribute verwendet. Ich benutzte eigentlich nur <div> mit data-*-Attributen und viel JavaScript. Mache ich etwas falsch?

Im Prinzip ist das durchaus ein Fehler. Wer keine HTML5-Elemente verwendet, verwendet andere Elemente, und über diese sagt die Spezifikation: Authors must not use elements [...] for purposes other than their intended semantic purpose. Und über das <div>-Element im Speziellen heißt es: Authors are strongly encouraged to view the div element as an element of last resort, for when no other element is suitable.

Ob man sich mit der Verletzung dieser Regeln wirklich einen wahrnehmbaren Nachteil einhandelt, steht freilich auf einem anderen Blatt. Jenseits aller semantischer Theorie besteht der Hauptunterschied zwischen den neuen semantischen HTML5-Elementen und dem guten alten <div> in den bei den neuen Elementen eingebauten Barrierefreiheits-Features. Ob man auf diese nun gesteigerten Wert legt oder nicht – es spricht nichts dagegen, diese einfach mitzunehmen und statt die <div>-Suppen mit ein paar <nav>- und <section>-Elementen zu dekorieren. Es kostet schließlich nichts.

Source Maps und mehrere Kompilier-Schritte

Source Maps sind schön und gut, aber was tun bei mehreren Kompilier-Schritten? Wenn ich z.B. mit Babel transpilierten Code erst durch Browserify und dann UglifyJS jagen möchte, bekomme ich doch keine Source Map mehr auf den Ausgangs-Quellcode mehr, oder?

Aber klar geht das! Die meisten der genannten Tools unterstützen Input-Source-Maps, UglifyJS z.B. über den Parameter --in-source-map. Diese Source Map wird dann als Ausgangslage für alle folgenden Transformationen genommen. Bei Inline-Maps (d.h. wenn die Source Map als Data-URL direkt in die JavaScript-Datei geschrieben wird) machen das die meisten Tools sogar automatisch. Und falls das mal nicht der Fall ist, kann man mittels Exorcist die Inline-Maps aus der Datei extrahieren und diese dann wiederum als Input für andere Tools nutzen.

Das <a>-Element in HTML5

Ist <ul><a href="#test"><li>Test</li></a></ul> gültiges HTML5? Es sieht nicht so aus, aber ich dachte bis gerade eben, man könnte in HTML5 um alles ein <a>-Element legen …

Nicht ganz: in HTML5 kann ein <a>-Element fast alles enthalten. Vor HTML5 waren HTML-Elemente in die zwei Kategorien Block-Elemente und Inline-Elemente eingeteilt und es galt die Regel: Inline-Elemente (wie <a>) dürfen keine Block-Elemente enthalten.

In HTML5 ist die Kategorisierung viel feinteiliger ausgefallen. Es gibt insgesamt sieben wichtige Sorten von Inhaltskategorien, wobei ein Element in mehrere Kategorien fallen kann. In welche Kategorie ein Element fällt, welche Elemente es enthalten kann und in welchen anderen Elementen es auftauchen darf, ist für das betroffene jeweils individuell Element definiert. Das <a>-Element darf beispielsweise überall auftauchen, wo sogenannter phrasing content erlaubt ist und es kann alles enthalten, was sein Elternelement enthalten darf – solange es sich nicht um interactive content, d.h. Inputs, andere Links etc. handelt.

Das <ul>-Element fällt in die Kategorie flow content. Es darf also auch in <a>-Elementen vorkommen, vorausgesetzt das Elternelements des <a>-Elements erlaubt flow content als Inhalt. Das war früher noch anders, denn da galten einfach <a> als Inline- und <ul> als Block-Element – die Kombination <a><ul></ul></a> war also nicht erlaubt. In HTML5 ist das hingegen kein Problem. Allerdings dürfen <ul> und auch <ol> weiterhin nur <li>-Elemente und Scripts als ihre Kindelemente haben.

Whitespace in HTML5

Definiert HTML5 irgendwo, wie der Browser mit Whitespace am Anfang oder Ende eines Elements umgehen soll? Sollten beispielsweise folgende Elemente immer gleich dargestellt werden (wobei „text“ hier für jedes Element mit Text darin stehen kann)?

text
[leerzeichen]text
text[leerzeichen]
[leerzeichen]text[leerzeichen]

Die Frage definiert HTML5 [kleines HTML-Syntax-Detail] … ist eigentlich immer mit Ja zu beantworten. HTML5 definiert den HTML-Parser bis ins kleinste Detail und dazu gehört auch die Verarbeitung von Whitespace.

Whitespace vor/nach/zwischen Elementen ist erlaubt (Inter-element whitespace) und führt zur Erzeugung von Textknoten mit nichts als eben dem Whitespace darin. Wenn wir im o.g. Beispiel mal <span>-Elemente als Beispiel-Non-Whitespace-Elemente hernehmen, sollte immer dieser DOM-Baum produziert werden:

  1. <span>A​</span>
  2. Textknoten "↵ " (Zeilenumbruch + führendes Leerzeichen)
  3. <span>B​</span>​
  4. Textknoten "↵" (Zeilenumbruch)
  5. <span>C​</span>​
  6. Textknoten " ↵ " (hinteres Leerzeichen + Zeilenumbruch + führendes Leerzeichen)
  7. <span>C​</span>​
  8. Textknoten " " (hinteres Leerzeichen)

Voraussetzung ist natürlich ein Browser mit ordentlichem HTML5-Parser an Bord.

Das Rendering dieser Whitespace-Textknoten ist durch die CSS-Eigenschaft white-space und nicht HTML5 definiert. Dort ist als Standardverhalten festgelegt, dass benachbarte Leerzeichen zusammengefasst bzw. alle bis auf eins auf eine Breite von 0 gequetscht werden. Mit anderen Werte für die white-space-Eigenschaft kann man das aber bequem den eigenen Wünschen anpassen.

Weitere Fragen?

All diese Fragen wurden mir per E-Mail oder Twitter gestellt und auch eure Fragen zu HTML(5), CSS(3), JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach über einen der genannten Kanäle anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.

Erklärbär-Termine für Juli, August und September

Veröffentlicht am 15. Juni 2015

In den nächsten Monaten bin ich vor allem mit Inhouse-Schulungen beschäftigt, aber es gibt auch wieder einen öffentlichen Erklärbär-Termin:

  • 3. - 5. August in München: Moderne Frontendentwicklung (HTML5, CSS3, JavaScript). HTML5 … und dann? Die Schulung „Moderne Frontendentwicklung“ ist der Nachfolger meiner klassischen HTML5-Schulung und hebt erfahrene Webentwickler auf das nächste Level. Der Kurs behandelt die seit HTML5 neu hinzugekommenen Webstandards (z.B. Web Components), bespricht Tools und Best Practices, gibt Tipps für den Einsatz neuer Features in heutigen Browsern und bietet auch einen Ausblick in die weitere Zukunft der Web-Plattform.

Termine unpassend, Orte alle zu weit weg und Programme nicht genehm? Ich komme auch gerne mit einem maßgeschneiderten Workshop vorbei – mich kann man ganz einfach mieten!