Mobile-First CSS: Is It Time for a Rethink?

Mike's Notes

I'm thinking ahead here. The current priority is first building Pipi websites for desktops and laptops because Pipi's job is building work tools. Later on, mobile apps will need to be catered for.

Resources

Mobile-First CSS: Is It Time for a Rethink?

By: Patrick Clancey
A List Apart: June 08, 2022

The mobile-first design methodology is great—it focuses on what really matters to the user, it’s well-practiced, and it’s been a common design pattern for years. So developing your CSS mobile-first should also be great, too…right? 

Well, not necessarily. Classic mobile-first CSS development is based on the principle of overwriting style declarations: you begin your CSS with default style declarations, and overwrite and/or add new styles as you add breakpoints with min-width media queries for larger viewports (for a good overview see “What is Mobile First CSS and Why Does It Rock?”). But all those exceptions create complexity and inefficiency, which in turn can lead to an increased testing effort and a code base that’s harder to maintain. Admit it—how many of us willingly want that?

On your own projects, mobile-first CSS may yet be the best tool for the job, but first you need to evaluate just how appropriate it is in light of the visual design and user interactions you’re working on. To help you get started, here’s how I go about tackling the factors you need to watch for, and I’ll discuss some alternate solutions if mobile-first doesn’t seem to suit your project.

Advantages of mobile-first

Some of the things to like with mobile-first CSS development—and why it’s been the de facto development methodology for so long—make a lot of sense:

  • Development hierarchy. One thing you undoubtedly get from mobile-first is a nice development hierarchy—you just focus on the mobile view and get developing. 
  • Tried and tested. It’s a tried and tested methodology that’s worked for years for a reason: it solves a problem really well.
  • Prioritizes the mobile view. The mobile view is the simplest and arguably the most important, as it encompasses all the key user journeys, and often accounts for a higher proportion of user visits (depending on the project). 
  • Prevents desktop-centric development. As development is done using desktop computers, it can be tempting to initially focus on the desktop view. But thinking about mobile from the start prevents us from getting stuck later on; no one wants to spend their time retrofitting a desktop-centric site to work on mobile devices!

Disadvantages of mobile-first

Setting style declarations and then overwriting them at higher breakpoints can lead to undesirable ramifications:
  • More complexity. The farther up the breakpoint hierarchy you go, the more unnecessary code you inherit from lower breakpoints. 
  • Higher CSS specificity. Styles that have been reverted to their browser default value in a class name declaration now have a higher specificity. This can be a headache on large projects when you want to keep the CSS selectors as simple as possible.
  • Requires more regression testing. Changes to the CSS at a lower view (like adding a new style) requires all higher breakpoints to be regression tested.
  • The browser can’t prioritize CSS downloads. At wider breakpoints, classic mobile-first min-width media queries don’t leverage the browser’s capability to download CSS files in priority order.

The problem of property value overrides

There is nothing inherently wrong with overwriting values; CSS was designed to do just that. Still, inheriting incorrect values is unhelpful and can be burdensome and inefficient. It can also lead to increased style specificity when you have to overwrite styles to reset them back to their defaults, something that may cause issues later on, especially if you are using a combination of bespoke CSS and utility classes. We won’t be able to use a utility class for a style that has been reset with a higher specificity.

With this in mind, I’m developing CSS with a focus on the default values much more these days. Since there’s no specific order, and no chains of specific values to keep track of, this frees me to develop breakpoints simultaneously. I concentrate on finding common styles and isolating the specific exceptions in closed media query ranges (that is, any range with a max-width set). 

This approach opens up some opportunities, as you can look at each breakpoint as a clean slate. If a component’s layout looks like it should be based on Flexbox at all breakpoints, it’s fine and can be coded in the default style sheet. But if it looks like Grid would be much better for large screens and Flexbox for mobile, these can both be done entirely independently when the CSS is put into closed media query ranges. Also, developing simultaneously requires you to have a good understanding of any given component in all breakpoints up front. This can help surface issues in the design earlier in the development process. We don’t want to get stuck down a rabbit hole building a complex component for mobile, and then get the designs for desktop and find they are equally complex and incompatible with the HTML we created for the mobile view! 

Though this approach isn’t going to suit everyone, I encourage you to give it a try. There are plenty of tools out there to help with concurrent development, such as Responsively App, Blisk, and many others. 

Having said that, I don’t feel the order itself is particularly relevant. If you are comfortable with focusing on the mobile view, have a good understanding of the requirements for other breakpoints, and prefer to work on one device at a time, then by all means stick with the classic development order. The important thing is to identify common styles and exceptions so you can put them in the relevant stylesheet—a sort of manual tree-shaking process! Personally, I find this a little easier when working on a component across breakpoints, but that’s by no means a requirement.

Closed media query ranges in practice 

In classic mobile-first CSS we overwrite the styles, but we can avoid this by using media query ranges. To illustrate the difference (I’m using SCSS for brevity), let’s assume there are three visual designs: 

  • smaller than 768
  • from 768 to below 1024
  • 1024 and anything larger 

Take a simple example where a block-level element has a default padding of “20px,” which is overwritten at tablet to be “40px” and set back to “20px” on desktop.

Classic min-width mobile-first

.my-block {
  padding: 20px;
  @media (min-width: 768px) {
    padding: 40px;
  }
  @media (min-width: 1024px) {
    padding: 20px;
  }
}

Closed media query range

.my-block {
  padding: 20px;
  @media (min-width: 768px) and (max-width: 1023.98px) {
    padding: 40px;
  }
}
The subtle difference is that the mobile-first example sets the default padding to “20px” and then overwrites it at each breakpoint, setting it three times in total. In contrast, the second example sets the default padding to “20px” and only overrides it at the relevant breakpoint where it isn’t the default value (in this instance, tablet is the exception).

The goal is to:

  • Only set styles when needed. 
  • Not set them with the expectation of overwriting them later on, again and again.

To this end, closed media query ranges are our best friend. If we need to make a change to any given view, we make it in the CSS media query range that applies to the specific breakpoint. We’ll be much less likely to introduce unwanted alterations, and our regression testing only needs to focus on the breakpoint we have actually edited. 

Taking the above example, if we find that .my-block spacing on desktop is already accounted for by the margin at that breakpoint, and since we want to remove the padding altogether, we could do this by setting the mobile padding in a closed media query range.

.my-block {
  @media (max-width: 767.98px) {
    padding: 20px;
  }
  @media (min-width: 768px) and (max-width: 1023.98px) {
    padding: 40px;
  }
}
The browser default padding for our block is “0,” so instead of adding a desktop media query and using unset or “0” for the padding value (which we would need with mobile-first), we can wrap the mobile padding in a closed media query (since it is now also an exception) so it won’t get picked up at wider breakpoints. At the desktop breakpoint, we won’t need to set any padding style, as we want the browser default value.

Bundling versus separating the CSS

Back in the day, keeping the number of requests to a minimum was very important due to the browser’s limit of concurrent requests (typically around six). As a consequence, the use of image sprites and CSS bundling was the norm, with all the CSS being downloaded in one go, as one stylesheet with highest priority. 

With HTTP/2 and HTTP/3 now on the scene, the number of requests is no longer the big deal it used to be. This allows us to separate the CSS into multiple files by media query. The clear benefit of this is the browser can now request the CSS it currently needs with a higher priority than the CSS it doesn’t. This is more performant and can reduce the overall time page rendering is blocked.

Which HTTP version are you using?

To determine which version of HTTP you’re using, go to your website and open your browser’s dev tools. Next, select the Network tab and make sure the Protocol column is visible. If “h2” is listed under Protocol, it means HTTP/2 is being used. 

Note: to view the Protocol in your browser’s dev tools, go to the Network tab, reload your page, right-click any column header (e.g., Name), and check the Protocol column.

Note: for a summarized comparison, see ImageKit’s “HTTP/2 vs. HTTP/1.”

Also, if your site is still using HTTP/1...WHY?!! What are you waiting for? There is excellent user support for HTTP/2.

Splitting the CSS

Separating the CSS into individual files is a worthwhile task. Linking the separate CSS files using the relevant media attribute allows the browser to identify which files are needed immediately (because they’re render-blocking) and which can be deferred. Based on this, it allocates each file an appropriate priority.

In the following example of a website visited on a mobile breakpoint, we can see the mobile and default CSS are loaded with “Highest” priority, as they are currently needed to render the page. The remaining CSS files (print, tablet, and desktop) are still downloaded in case they’ll be needed later, but with “Lowest” priority. 

With bundled CSS, the browser will have to download the CSS file and parse it before rendering can start.

While, as noted, with the CSS separated into different files linked and marked up with the relevant media attribute, the browser can prioritize the files it currently needs. Using closed media query ranges allows the browser to do this at all widths, as opposed to classic mobile-first min-width queries, where the desktop browser would have to download all the CSS with Highest priority. We can’t assume that desktop users always have a fast connection. For instance, in many rural areas, internet connection speeds are still slow. 

The media queries and number of separate CSS files will vary from project to project based on project requirements, but might look similar to the example below.

Bundled CSS

<link href="site.css" rel="stylesheet">

This single file contains all the CSS, including all media queries, and it will be downloaded with Highest priority.

Separated CSS

<link href="default.css" rel="stylesheet"><link href="mobile.css" media="screen and (max-width: 767.98px)" rel="stylesheet"><link href="tablet.css" media="screen and (min-width: 768px) and (max-width: 1083.98px)" rel="stylesheet"><link href="desktop.css" media="screen and (min-width: 1084px)" rel="stylesheet"><link href="print.css" media="print" rel="stylesheet">

Separating the CSS and specifying a media attribute value on each link tag allows the browser to prioritize what it currently needs. Out of the five files listed above, two will be downloaded with Highest priority: the default file, and the file that matches the current media query. The others will be downloaded with Lowest priority.

Depending on the project’s deployment strategy, a change to one file (mobile.css, for example) would only require the QA team to regression test on devices in that specific media query range. Compare that to the prospect of deploying the single bundled site.css file, an approach that would normally trigger a full regression test.

Moving on

The uptake of mobile-first CSS was a really important milestone in web development; it has helped front-end developers focus on mobile web applications, rather than developing sites on desktop and then attempting to retrofit them to work on other devices.

I don’t think anyone wants to return to that development model again, but it’s important we don’t lose sight of the issue it highlighted: that things can easily get convoluted and less efficient if we prioritize one particular device—any device—over others. For this reason, focusing on the CSS in its own right, always mindful of what is the default setting and what’s an exception, seems like the natural next step. I’ve started noticing small simplifications in my own CSS, as well as other developers’, and that testing and maintenance work is also a bit more simplified and productive. 

In general, simplifying CSS rule creation whenever we can is ultimately a cleaner approach than going around in circles of overrides. But whichever methodology you choose, it needs to suit the project. Mobile-first may—or may not—turn out to be the best choice for what’s involved, but first you need to solidly understand the trade-offs you’re stepping into.

High Performance Browser Networking

Mike's Notes

Ilya Grigorik’s book High Performance Browser Networking provides valuable guidelines and advice on making websites fast. It’s also available as a free HTML book. The table of Contents (TOC) is below.

Ilya Grigorik is a web performance engineer at Google and co-chair of the W3C Web Performance Working Group.

Resources

High Performance Browser Networking

By: Ilya Grigorik
O'Reilly: 

Performance is a feature. This book provides a hands-on overview of what every web developer needs to know about the various types of networks (WiFi, 3G/4G), transport protocols (UDP, TCP, and TLS), application protocols (HTTP/1.1, HTTP/2), and APIs available in the browser (XHR, WebSocket, WebRTC, and more) to deliver the best—fast, reliable, and resilient—user experience.

Table of Contents

Networking 101

  • Primer on Latency and Bandwidth
    • Speed Is a Feature
    • The Many Components of Latency
    • Speed of Light and Propagation Latency
    • Last-Mile Latency
    • Bandwidth in Core Networks
    • Bandwidth at the Network Edge
    • Delivering Higher Bandwidth and Lower Latencies
  • Building Blocks of TCP
    • Three-Way Handshake
    • Congestion Avoidance and Control
    • Bandwidth-Delay Product
    • Head-of-Line Blocking
    • Optimizing for TCP
  • Building Blocks of UDP
    • Null Protocol Services
    • UDP and Network Address Translators
    • Optimizing for UDP
  • Transport Layer Security (TLS)
    • Encryption, Authentication, and Integrity
    • HTTPS Everywhere
    • TLS Handshake
    • TLS Session Resumption
    • Chain of Trust and Certificate Authorities
    • Certificate Revocation
    • TLS Record Protocol
    • Optimizing for TLS
    • Testing and Verification

Performance of Wireless Networks

  • Introduction to Wireless Networks
    • Ubiquitous Connectivity
    • Types of Wireless Networks
    • Performance Fundamentals of Wireless Networks
    • Measuring Real-World Wireless Performance
  • WiFi
    • From Ethernet to a Wireless LAN
    • WiFi Standards and Features
    • Measuring and Optimizing WiFi Performance
    • Optimizing for WiFi Networksobile Networks
  • Brief History of the G’s
    • Device Features and Capabilities
    • Radio Resource Controller (RRC)
    • End-to-End Carrier Architecture
    • Packet Flow in a Mobile Network
    • Heterogeneous Networks (HetNets)
    • Real-World 3G, 4G, and WiFi Performance
  • Optimizing for Mobile Networks
    • Preserve Battery Power
    • Eliminate Periodic and Inefficient Data Transfers
    • Anticipate Network Latency Overhead
    • Design for Variable Network Interface Availability
    • Burst Your Data and Return to Idle
    • Offload to WiFi Networks
    • Apply Protocol and Application Best Practices

HTTP

  • Brief History of HTTP
    • HTTP 0.9: The One-Line Protocol
    • HTTP/1.0: Rapid Growth and Informational RFC
    • HTTP/1.1: Internet Standard
    • HTTP/2: Improving Transport Performance
  • Primer on Web Performance
    • Hypertext, Web Pages, and Web Applications
    • Anatomy of a Modern Web Application
    • Performance Pillars: Computing, Rendering, Networking
    • Synthetic and Real-User Performance Measurement
    • Browser Optimization
  • HTTP/1.X
    • Benefits of Keepalive Connections
    • HTTP Pipelining
    • Using Multiple TCP Connections
    • Domain Sharding
    • Measuring and Controlling Protocol Overhead
    • Concatenation and Spriting
    • Resource Inlining
  • HTTP/2
    • Brief History of SPDY and HTTP/2
    • Design and Technical Goals
    • Binary Framing Layer
    • Streams, Messages, and Frames
    • Request and Response Multiplexing
    • Stream Prioritization
    • One Connection Per Origin
    • Flow Control
    • Server Push
    • Header Compression
    • Upgrading to HTTP/2
    • Brief Introduction to Binary Framing
  • Optimizing Application Delivery
    • Optimizing Physical and Transport Layers
    • Evergreen Performance Best Practices
    • Optimizing for HTTP/1.x
    • Optimizing for HTTP/2

Browser APIs and Protocols

  • Primer on Browser Networking
    • Connection Management and Optimization
    • Network Security and Sandboxing
    • Resource and Client State Caching
    • Application APIs and Protocols
  • XMLHttpRequest
    • Brief History of XHR
    • Cross-Origin Resource Sharing (CORS)
    • Downloading Data with XHR
    • Uploading Data with XHR
    • Monitoring Download and Upload Progress
    • Streaming Data with XHR
    • +Real-Time Notifications and Delivery
    • XHR Use Cases and Performance
  • Server-Sent Events (SSE)
    • EventSource API
    • Event Stream Protocol
    • SSE Use Cases and Performance
  • WebSocket+
    • WebSocket API+
    • WebSocket Protocol+
    • WebSocket Use Cases and Performance
    • Performance Checklist
  • WebRTC
    • Standards and Development of WebRTC+
    • Audio and Video Engines+
    • Real-Time Network Transports+
    • Establishing a Peer-to-Peer Connection+
    • Delivering Media and Application Data+
    • DataChannel+
    • WebRTC Use Cases and Performance
    • Performance Checklist

Some Reflections on Pipi

Mike's Notes

I wrote this to answer a great question from a friend. As a growing community of supporters forms around Pipi, many questions arise.

Thank you for the question, Alex.

Resources

Some reflections on Pipi

By: Mike Peters
26/12/2024

Mike is the inventor and architect of Pipi and the founder of Ajabbi.

Origin

Pipi originated in 1997 with version 1, and 28 years later, it is version 9. You can read about its working history here.

Today

Pipi 9 has become like a CNC machine built to automatically create socially useful SaaS (cloud software) applications for large, complex systems that are as simple as possible for people to administer and use.

These potential systems include health, education, public transport, drinking water, sewerage, and electrical power.

Free Open Software

One day, those applications, including the databases, code, and HTML, will be freely available on GitHub without reuse restriction. Unlike Pipi, they will use an MCV architecture. Reusing existing frameworks for Pipi to build the applications may be possible, including using different languages and database programs, such as CFML, PHP, Java, Python, MS SQL, Postgres, and Oracle.

Pipi Platform

Pipi (the CNC machine) is huge (thousands of tables), highly complex, with autonomous systems, and self-managing. It will remain closed-source and hidden.

Documentation

Pipi generates its own self-documentation at:

This growing 5,000 static pages of (95% auto-generated) documentation is more of an experimental mockup for testing usefulness than an accurate one, but that will change soon.

Example

Please look at a representative web page for one of the engines.

If you go to 

There is a list of tables that is made up. Eventually, there will be a real list using alias names and real descriptions.

View Windows

You will see a series of half-empty iFrames halfway down, e.g., Fibre Connect.

These will eventually act as a view window into some hidden live process, such as a moving chart, log, or code generation. There will be many more windows to come.

Applications

Eventually, logged-on users will create new no-code applications by configuring Pipi's parameters via web forms, importing ontologies, creating plugins, connecting to APIs, etc. However, data and code constraints are imposed on Pipi at a deeper, hidden level to restrict such applications to those of broad social benefit, e.g., health, education, public transport, etc.

Security

I believe Pipi could be a powerful tool misused by crooks involved in drug dealing, child pornography, social media bullying, privacy invasion, cybercrime, terrorism and other horrible things. I am not going to let that happen. The only way to do this is by restricting access to Pipi itself.

Shared Model

The underlying model behind each application is shared. Any feature a user creates is automatically shared with all users as an available option. Similar to Wikipedia, edits will follow a community process. This excludes customisation, which is a private matter.

I have built and will build multiple applications based on Len Silverstons's Universal Data Models to kick-start this community process.

Accounts

Alongside the freely available SaaS applications on GitHub, paid accounts will also be available for organisations with significant, complex needs, where Pipi manages the installation and updates to the same applications. Think of it as a superwizard process.

An example of this might be part of a national health system.

Four types of accounts have been created and will be available with different features, costs, permissions, roles, and database access.

  • Individual (free)
  • SME (usage-based, tenanted shared database, similar to Xero)
  • DevOps (usage-based, for developers to develop and administer Enterprise accounts)
  • Enterprise (usage-based, highly configurable, dedicated databases, requires a DevOps account to provide support)
  • Pipi (autonomous, super admin)
Other account types can be added if needed.

Costs

This usage-based income will cover cloud hosting costs, robust infrastructure, external services (legal and accounting), and other necessary expenses to provide excellent, reliable service. 

Foundation

Any surplus will go to a future Ajabbi Foundation (similar to the Linux or Apache Foundations) to support user groups, publish books for developers and users, run conferences, provide training, etc.

Open Research

Some funds will also support a future Ajabbi Research institution (a bit like Microsoft Research) to look at more profound questions around complexity, emergence, science, mathematics, dialectics and nature using computers and ways to expand Pipi's usefulness for humanity that may be discovered.

Collaborations will be sought with other research institutions, with all research results freely available at no cost.

There is an initial list of such people who it would be good to work with in future;

Some activities could be providing research grants, organising conferences, and publishing books.

And throughout this, all human interactions with Pipi will be via a simple web interface.

Respecting People

To protect Pipi from the Cancel Culture and other self-serving forms of privileged parasitism, we should have open books so every cent is accountable and write an open handbook so everyone can access the same information and express their opinions. This is a first draft.

Attitude First

In the words of Dr Martin Luther King in Selma 1963.

"I have a dream that my four little children will one day live in a nation where they will not be judged by the color of their skin but by the content of their character. I have a dream today!"

Summary

This project will never involve investors or private equity firms. It is a self-funding, 100% public good effort to make great SaaS software for complex systems available to everyone at a low cost, regardless of where they live, who they are or their language and beliefs.

You are most welcome to be involved.

Pipi Build Roadmap

Mike's Notes

At the beginning of 2023, Pipi 9 was working software without a front end (headless), no user interface and completely undocumented.

A project began last year to get Pipi to self-document itself by generating static web pages using templates driven by database content. This has gone well; the website now has 5,000 pages. Given Pipi's complex internal architecture and large code base, the documentation will eventually be around 20,000 pages in English. One day, there will also be documentation sets in other languages, so reliable automation is crucial.

Enough progress has been made to publish a realistic roadmap for the next few months. It is updated daily and follows steady progress.

I'm currently sorting out the correct names for everything and short descriptions with proper sentences. This will stabilise the web URLs, allowing Google to index the documentation. Searchers will not end up with broken links.

Then, automatic versioning and current status will be added.

This will be followed by user interfaces (UI) for logged-in users by generating dedicated workspaces. 

I will then be able to administer Pipi using an HTML front end, which will be much easier than tweaking the config files and reading logs.

After that happens, users can discuss, add references, and directly edit the human-editable parts of the wiki documentation.

Eventually, Pipi will become my IDE, which will significantly speed things up.

And so on. For more details, read the roadmap.

I thank my friends Alex and Chris for asking enough questions to help me develop something useful. The roadmap started with a Google Sheets draft and is now published on the developer's website.

Resources

Moving Naturalism Forward

Mike's Notes

My friend James Miller sent me a link to this workshop recording. Three days of discussion on YouTube. 

Sean Carroll, Jerry Coyne, Richard Dawkins, Terrence Deacon, Simon DeDeo, Daniel Dennett, Owen Flanagan, Rebecca Goldstein, Janna Levin, David Poeppel, Massimo Pigliucci, Nicholas Pritzker, Alex Rosenberg, Don Ross, and Steven Weinberg are among the participants.

I have been watching the whole series.

This information comes from the website of physicist Sean Carroll.

Resources

Moving Naturalism Forward

An Interdisciplinary Workshop - Oct. 25-29, 2012

Over four centuries of scientific progress have convinced most professional philosophers and scientists of the validity of naturalism: the view that there is only one realm of existence, the natural world, whose behavior can be studied through reason and empirical investigation. The basic operating principles of the natural world appear to be impersonal and inviolable; microscopic constituents of inanimate matter obeying the laws of physics fit together in complex structures to form intelligent, emotive, conscious human beings.

In the public sphere, debates continue between naturalism and spiritual or religious or dualistic worldviews, and those debates are worth having. But it is also important for those committed to naturalism to address the very difficult questions raised by replacing folk psychology and morality by a scientifically-grounded understanding of reality. We would like to understand how to construct meaningful human lives in a world governed by the laws of nature. Some specific questions include:

  • Free will. If people are collections of atoms obeying the laws of physics, is it sensible to say that they make choices?
  • Morality. What is the origin of right and wrong? Are there objective standards?
  • Meaning. Why live? Is there a rational justification for finding meaning in human existence?
  • Purpose. Do teleological concepts play a useful role in our description of natural phenomena?
  • Epistemology. Is science unique as a method for discovering true knowledge?
  • Emergence. Does reductionism provide the best path to understanding complex systems, or do different levels of description have autonomous existence?
  • Consciousness. How do the phenomena of consciousness arise from the collective behavior of inanimate matter?
  • Evolution. Can the ideas of natural selection be usefully extended to areas outside of biology, or can evolution be subsumed within a more general theory of complex systems?
  • Determinism. To what extent is the future determined given quantum uncertainty and chaos theory, and does it matter?

This workshop brought together a small number of researchers and writers to tackle the project of moving naturalism forward by making progress on these issues. We met for three days of focused discussion and debate at the Red Lion Inn in Stockbridge, Massachusetts. Full videos of the proceedings are freely available online.

Free IO Tools

Mike's Notes

This one popped up in Smashing Magazine this morning. A fantastic suite of online utility tools.

Resources

Tools To Simplify Everyday Tasks

Smashing Magazine: Issue #488 • Dec 24, 2024

By: Cosima Mielke

You provide the input, they provide the output as a result, that’s the idea behind IO Tools. IO Tools is an extensive suite of useful web apps for designers, developers, authors, marketers, analysts, and even gamers.

Whether you’re looking for an empty lines remover, a space-to-tab converter, a text compare tool, or a name generator, with more than 300 tools available, chances are good that IO Tools has just what you need.

 You can use the tools for free; however, some of them, like AI tools, consume credits (you get 50 free credits daily). One for the bookmarks. (cm)

Statistical Laws in Complex Systems: Combining Mechanistic Models and Data Analysis by Eduardo G. Altmann

Mike's Notes

I discovered this fascinating book/paper by Eduardo G. Altmann listed in the weekly Complexity Digest.

"Provides an unifying approach to the study of statistical laws. Starts from simple examples and goes through more advanced time-series and statistical methods. Presents the necessary material to analyze, test, and interpret results in existing and new datasets" - Complexity Digest

He is a Maths and Stats Professor in the Complex Systems and Data Science group at the School of Mathematics and Statistics at the University of Sydney, Australia.

Resources

Statistical Laws in Complex Systems: Combining Mechanistic Models and Data Analysis

By: Eduardo G. Altmann

ArXiv: Submitted on 29 Jul 2024


Summary

Statistical laws describe regular patterns observed in diverse scientific domains, ranging from the magnitude of earthquakes (Gutenberg-Richter law) and metabolic rates in organisms (Kleiber's law), to the frequency distribution of words in texts (Zipf's and Herdan-Heaps' laws), and productivity metrics of cities (urban scaling laws). The origins of these laws, their empirical validity, and the insights they provide into underlying systems have been subjects of scientific inquiry for centuries. This monograph provides an unifying approach to the study of statistical laws, critically evaluating their role in the theoretical understanding of complex systems and the different data-analysis methods used to evaluate them. Through a historical review and a unified analysis, we uncover that the persistent controversies on the validity of statistical laws are predominantly rooted not in novel empirical findings but in the discordance among data-analysis techniques, mechanistic models, and the interpretations of statistical laws. Starting with simple examples and progressing to more advanced time-series and statistical methods, this monograph and its accompanying repository provide comprehensive material for researchers interested in analyzing data, testing and comparing different laws, and interpreting results in both existing and new datasets.

Read the 152 page pdf

The Spilhaus World Ocean in a Square map

Mike's Notes

The Spilhaus World Ocean in a Square map is an excellent map projection that centres on the seven oceans with distortion of land. It is terrific for understanding ocean circulation.

This map was shared by Huw Griffiths.



The maps below are are by John Nelson of ESRI.

Resources

The Spilhaus World Ocean in a Square map

Wikipedia:

In 1942 Spilhaus tackled the problem of displaying the world's oceans in an unbroken view. He achieved this by carefully selecting antipodal points as the centers for two hemispheric projections. However it wasn't until 1979 that he published maps using continental shorelines as "natural boundaries", including one that has become the typical example of Spilhaus's technique. It uses locations near Hankou in China and Córdoba in Argentina as poles with a cut joining them across the Bering Strait. In 1991, Spilhaus published Atlas of the World illustrated with a large selection of maps having "geophysical boundaries", typically coastlines, in various orientations and for various purposes. He published several other papers and articles on the topic.






CSS Inheritance, The Cascade And Global Scope: Your New Old Worst Best Friends

Mike's Notes

An interesting article in Smashing Magazine about CSS.

Resources

CSS Inheritance, The Cascade And Global Scope: Your New Old Worst Best Friends

By: Heydon Pickering
Smashing Magazine: Nov 21, 2016

Quick Summary

If you don’t want your design to look like it’s made out of unrelated things, this article is for you. There is already a technology, called CSS, which is designed specifically to solve this problem. Using CSS, you can propagate styles that cross the borders of your HTML components, ensuring a consistent design with minimal effort. Today, Heydon Pickering is going to revisit inheritance, the cascade and scope here with respect to modular interface design. He aims to show you how to leverage these features so that your CSS code becomes more concise and self-regulating, and your interface more easily extensible.

I’m big on modular design. I’ve long been sold on dividing websites into components, not pages, and amalgamating those components dynamically into interfaces. Flexibility, efficiency and maintainability abound.

But I don’t want my design to look like it’s made out of unrelated things. I’m making an interface, not a surrealist photomontage. As luck would have it, there is already a technology, called CSS, which is designed specifically to solve this problem. Using CSS, I can propagate styles that cross the borders of my HTML components, ensuring a consistent design with minimal effort.

  • inheritance,
  • the cascade (the “C” in CSS).

Despite these features enabling a DRY, efficient way to style web documents and despite them being the very reason CSS exists, they have fallen remarkably out of favor. From CSS methodologies such as BEM and Atomic CSS through to programmatically encapsulated CSS modules, many are doing their best to sidestep or otherwise suppress these features. This gives developers more control over their CSS, but only an autocratic sort of control based on frequent intervention.

I’m going to revisit inheritance, the cascade and scope here with respect to modular interface design. I aim to show you how to leverage these features so that your CSS code becomes more concise and self-regulating, and your interface more easily extensible.

Inheritance And font-family #

Despite protestations by many, CSS does not only provide a global scope. If it did, everything would look exactly the same. Instead, CSS has a global scope and a local scope. Just as in JavaScript, the local scope has access to the parent and global scope. In CSS, this facilitates inheritance.

For instance, if I apply a font-family declaration to the root (read: global) html element, I can ensure that this rule applies to all ancestor elements within the document (with a few exceptions, to be addressed in the next section).

html {
  font-family: sans-serif;
}
/*
This rule is not needed ↷
p {
  font-family: sans-serif;
}
*/

Just like in JavaScript, if I declare something within the local scope, it is not available to the global — or, indeed, any ancestral — scope, but it is available to the child scope (elements within p). In the next example, the line-height of 1.5 is not adopted by the html element. However, the a element inside the p does respect the line-height value.

html {
  font-family: sans-serif;
}
p {
  line-height: 1.5;
}
/*

This rule is not needed ↷

p a {
  line-height: 1.5;
}
*/

The great thing about inheritance is that you can establish the basis for a consistent visual design with very little code. And these styles will even apply to HTML you have yet to write. Talk about future-proof!

The Alternative #

There are other ways to apply common styles, of course. For example, I could create a .sans-serif class…

.sans-serif {
  font-family: sans-serif;
}

… and apply it to any element that I feel should have that style:

<p class="sans-serif">Lorem ipsum.</p>

This affords me some control: I can pick and choose exactly which elements take this style and which don’t.

Any opportunity for control is seductive, but there are clear issues. Not only do I have to manually apply the class to any element that should take it (which means knowing what the class is to begin with), but in this case I’ve effectively forgone the possibility of supporting dynamic content: Neither WYSIWYG editors nor Markdown parsers provide sans-serif classes to arbitrary p elements by default.

That class=“sans-serif” is not such a distant relative of style=“font-family: sans-serif” — except that the former means adding code to both the style sheet and the HTML. Using inheritance, we can do less of one and none of the other. Instead of writing out classes for each font style, we can just apply any we want to the html element in one declaration:

html {
  font-size: 125%;
  font-family: sans-serif;
  line-height: 1.5;
  color: #222;
}

The inherit Keyword #

Some types of properties are not inherited by default, and some elements do not inherit some properties. But you can use [property name]: inherit to force inheritance in some cases.

For example, the input element doesn’t inherit any of the font properties in the previous example. Nor does textarea. In order to make sure all elements inherit these properties from the global scope, I can use the universal selector and the inherit keyword. This way, I get the most mileage from inheritance.

* {
  font-family: inherit;
  line-height: inherit;
  color: inherit;
}
html {
  font-size: 125%;
  font-family: sans-serif;
  line-height: 1.5;
  color: #222;
}

Note that I’ve omitted font-size. I don’t want font-size to be inherited directly because it would override user-agent styles for heading elements, the small element and others. This way, I save a line of code and can defer to user-agent styles if I should want.

Another property I would not want to inherit is font-style: I don’t want to unset the italicization of ems just to code it back in again. That would be wasted work and result in more code than I need.

Now, everything either inherits or is forced to inherit the font styles I want them to. We’ve gone a long way to propagating a consistent brand, project-wide, with just two declaration blocks. From this point onwards, no developer has to even think about font-family, line-height or color while constructing components, unless they are making exceptions. This is where the cascade comes in.

Exceptions-Based Styling #

I’ll probably want my main heading to adopt the same font-family, color and possibly line-height. That’s taken care of using inheritance. But I’ll want its font-size to differ. Because the user agent already provides an enlarged font-size for h1 elements (and it will be relative to the 125% base font size I’ve set), it’s possible I don’t need to do anything here.

However, should I want to tweak the font size of any element, I can. I take advantage of the global scope and only tweak what I need to in the local scope.

* {
  font-family: inherit;
  line-height: inherit;
  color: inherit;
}
html {
  font-size: 125%;
  font-family: sans-serif;
  line-height: 1.5;
  color: #222;
}
h1 {
  font-size: 3rem;
}

If the styles of CSS elements were encapsulated by default, this would not be possible: I’d have to add all of the font styles to h1 explicitly. Alternatively, I could divide my styles up into separate classes and apply each to the h1 as a space-separated value:

<h1 class="Ff(sans) Fs(3) Lh(1point5) C(darkGrey)">Hello World</h1>

Either way, it’s more work and a styled h1 would be the only outcome. Using the cascade, I’ve styled most elements the way I want them, with h1 just as a special case, just in one regard. The cascade works as a filter, meaning styles are only ever stated where they add something new.

Element Styles #

We’ve made a good start, but to really leverage the cascade, we should be styling as many common elements as possible. Why? Because our compound components will be made of individual HTML elements, and a screen-reader-accessible interface makes the most of semantic markup.

To put it another way, the style of “atoms” that make up your interface “molecules” (to use atomic design terminology) should be largely addressable using element selectors. Element selectors are low in specificity, so they won’t override any class-based styles you might incorporate later.

The first thing you should do is style all of the elements that you know you’re going to need:

a { … }
p { … }
h1, h2, h3 { … }
input, textarea { … }
/* etc */
The next part is crucial if you want a consistent interface without redundancy: Each time you come to creating a new component, if it introduces new elements, style those new elements with element selectors. Now is not the time to introduce restrictive, high-specificity selectors. Nor is there any need to compose a class. Semantic elements are what they are.

For example, if I’ve yet to style button elements (as in the previous example) and my new component incorporates a button element, this is my opportunity to style button elements for the entire interface.

button {
  padding: 0.75em;
  background: #008;
  color: #fff;
}
button:focus {
  outline: 0.25em solid #dd0;
}

Now, when you come to write a new component that also happens to incorporate buttons, that’s one less thing to worry about. You’re not rewriting the same CSS under a different namespace, and there’s no class name to remember or write either. CSS should always aim to be this effortless and efficient — it’s designed for it.

Using element selectors has three main advantages:

  • The resulting HTML is less verbose (no redundant classes).
  • The resulting style sheet is less verbose (styles are shared between components, not rewritten per component).
  • The resulting styled interface is based on semantic HTML.

The use of classes to exclusively provide styles is often defended as a “separation of concerns.” This is to misunderstand the W3C’s separation of concerns principle. The objective is to describe structure with HTML and style with CSS. Because classes are designated exclusively for styling purposes and they appear within the markup, you are technically breaking with separation wherever they’re used. You have to change the nature of the structure to elicit the style.

Wherever you don’t rely on presentational markup (classes, inline styles), your CSS is compatible with generic structural and semantic conventions. This makes it trivial to extend content and functionality without it also becoming a styling task. It also makes your CSS more reusable across different projects where conventional semantic structures are employed (but where CSS ‘methodologies’ may differ).

Special Cases #

Before anyone accuses me of being simplistic, I’m aware that not all buttons in your interface are going to do the same thing. I’m also aware that buttons that do different things should probably look different in some way.

But that’s not to say we need to defer to classes, inheritance or the cascade. To make buttons found in one interface look fundamentally dissimilar is to confound your users. For the sake of accessibility and consistency, most buttons only need to differ in appearance by label.

<button>create</button>
<button>edit</button>
<button>delete</button>


Remember that style is not the only visual differentiator. Content also differentiates visually — and in a way that is much less ambiguous. You’re literally spelling out what different things are for.

There are fewer instances than you might imagine where using style alone to differentiate content is necessary or appropriate. Usually, style differences should be supplemental, such as a red background or a pictographic icon accompanying a textual label. The presence of textual labels are of particular utility to those using voice-activation software: Saying “red button” or “button with cross icon” is not likely to elicit recognition by the software.

I’ll cover the topic of adding nuances to otherwise similar looking elements in the “Utility Classes” section to follow.

Attributes #

Semantic HTML isn’t just about elements. Attributes define types, properties and states. These too are important for accessibility, so they need to be in the HTML where applicable. And because they’re in the HTML, they provide additional opportunities for styling hooks.

For example, the input element takes a type attribute, should you want to take advantage of it, and also attributes such as aria-invalid to describe state.

input, textarea {
  border: 2px solid;
  padding: 0.5rem;
}
[aria-invalid] {
  border-color: #c00;
  padding-right: 1.5rem;
  background: url(images/cross.svg) no-repeat center 0.5em;
}

A few things to note here:

I don’t need to set color, font-family or line-height here because these are inherited from html, thanks to my use of the inherit keyword. If I want to change the main font-family used application-wide, I only need to edit the one declaration in the html block.

The border color is linked to color, so it too inherits the global color. All I need to declare is the border’s width and style.

The [aria-invalid] attribute selector is unqualified. This means it has better reach (it can be used with both my input and textarea selectors) and it has minimal specificity. Simple attribute selectors have the same specificity as classes. Using them unqualified means that any classes written further down the cascade will override them as intended.

The BEM methodology would solve this by applying a modifier class, such as input–invalid. But considering that the invalid state should only apply where it is communicated accessibly, input–invalid is necessarily redundant. In other words, the aria-invalid attribute has to be there, so what’s the point of the class?

Just Write HTML #

My absolute favorite thing about making the most of element and attribute selectors high up in the cascade is this: The composition of new components becomes less a matter of knowing the company or organization’s naming conventions and more a matter of knowing HTML. Any developer versed in writing decent HTML who is assigned to the project will benefit from inheriting styling that’s already been put in place. This dramatically reduces the need to refer to documentation or write new CSS. For the most part, they can just write the (meta) language that they should know by rote. Tim Baxter also makes a case for this in Meaningful CSS: Style It Like You Mean It.

Layout #

So far, we’ve not written any component-specific CSS, but that’s not to say we haven’t styled anything. All components are compositions of HTML elements. It’s largely in the order and arrangement of these elements that more complex components form their identity.

Which brings us to layout.

Principally, we need to deal with flow layout — the spacing of successive block elements. You may have noticed that I haven’t set any margins on any of my elements so far. That’s because margin should not be considered a property of elements but a property of the context of elements. That is, they should only come into play where elements meet.

Fortunately, the adjacent sibling combinator can describe exactly this relationship. Harnessing the cascade, we can instate a uniform default across all block-level elements that appear in succession, with just a few exceptions.

* {
  margin: 0;
}
* + * {
  margin-top: 1.5em;
}
body, br, li, dt, dd, th, td, option {
  margin-top: 0;
}

The use of the extremely low-specificity lobotomized owl selector ensures that any elements (except the common exceptions) are spaced by one line. This means that there is default white space in all cases, and developers writing component flow content will have a reasonable starting point.

In most cases, margins now take care of themselves. But because of the low specificity, it’s easy to override this basic one-line spacing where needed. For example, I might want to close the gap between labels and their respective fields, to show they are paired. In the following example, any element that follows a label (input, textarea, select, etc.) closes the gap.

label {
  display: block
}
label + * {
  margin-top: 0.5rem;
}

Once again, using the cascade means only having to write specific styles where necessary. Everything else conforms to a sensible baseline.

Note that, because margins only appear between elements, they don’t double up with any padding that may have been included for the container. That’s one more thing not to have to worry about or code defensively against.

Also, note that you get the same spacing whether or not you decide to include wrapper elements. That is, you can do the following and achieve the same layout — it’s just that the margins emerge between the divs rather than between labels following inputs.

<form>
  <div>
    <label for="one">Label one</label>
    <input id="one" name="one" type="text">
  </div>
  <div>
    <label for="two">Label two</label>
    <input id="two" name="two" type="text">
  </div>
  <button type="submit">Submit</button>
</form>

Achieving the same result with a methodology such as atomic CSS would mean composing specific margin-related classes and applying them manually in each case, including for first-child exceptions handled implicitly by * + *:

<form class="Mtop(1point5)">
  <div class="Mtop(0)">
    <label for="one" class="Mtop(0)">Label one</label>
    <input id="one" name="one" type="text" class="Mtop(0point75)">
  </div>
  <div class="Mtop(1point5)">
    <label for="two" class="Mtop(0)">Label two</label>
    <input id="two" name="two" type="text" class="Mtop(0point75)">
  </div>
  <button type="submit" class="Mtop(1point5)">Submit</button>
</form>

Bear in mind that this would only cover top margins if one is adhering to atomic CSS. You’d have to prescribe individual classes for color, background-color and a host of other properties, because atomic CSS does not leverage inheritance or element selectors.

<form class="Mtop(1point5) Bdc(#ccc) P(1point5)">
  <div class="Mtop(0)">
    <label for="one" class="Mtop(0) C(brandColor) Fs(bold)">Label one</label>
    <input id="one" name="one" type="text" class="Mtop(0point75) C(brandColor) Bdc(#fff) B(2) P(1)">
  </div>
  <div class="Mtop(1point5)">
    <label for="two" class="Mtop(0) C(brandColor) Fs(bold)">Label two</label>
    <input id="two" name="two" type="text" class="Mtop(0point75) C(brandColor) Bdc(#fff) B(2) P(1)">
  </div>
  <button type="submit" class="Mtop(1point5) C(#fff) Bdc(blue) P(1)">Submit</button>
</form>
Atomic CSS gives developers direct control over style without deferring completely to inline styles, which are not reusable like classes. By providing classes for individual properties, it reduces the duplication of declarations in the stylesheet.

However, it necessitates direct intervention in the markup to achieve these ends. This requires learning and being commiting to its verbose API, as well as having to write a lot of additional HTML code.

Instead, by styling arbitrary HTML elements and their spacial relationships, CSS ‘methodology’ becomes largely obsolete. You have the advantage of working with a unified design system, rather than an HTML system with a superimposed styling system to consider and maintain separately.

Anyway, here’s how the structure of our CSS should look with our flow content solution in place:

  1. global (html) styles and enforced inheritance,
  2. flow algorithm and exceptions (using the lobotomized owl selector),
  3. element and attribute styles.

We’ve yet to write a specific component or conceive a CSS class, but a large proportion of our styling is done — that is, if we write our classes in a sensible, reusable fashion.

Utility Classes #

The thing about classes is that they have a global scope: Anywhere they are applied in the HTML, they are affected by the associated CSS. For many, this is seen as a drawback, because two developers working independently could write a class with the same name and negatively affect each other’s work.

CSS modules were recently conceived to remedy this scenario by programmatically generating unique class names tied to their local or component scope.

<!-- my module's button -->
<button class="button_dysuhe027653">Press me</button>

<!-- their module's button -->
<button class="button_hydsth971283">Hit me</button>

Ignoring the superficial ugliness of the generated code, you should be able to see where disparity between independently authored components can easily creep in: Unique identifiers are used to style similar things. The resulting interface will either be inconsistent or be consistent with much greater effort and redundancy.

There’s no reason to treat common elements as unique. You should be styling the type of element, not the instance of the element. Always remember that the term “class” means “type of thing, of which there may be many.” In other words, all classes should be utility classes: reusable globally.

Of course, in this example, a .button class is redundant anyway: we have the button element selector to use instead. But what if it was a special type of button? For instance, we might write a .danger class to indicate that buttons do destructive actions, like deleting data:

.danger {
  background: #c00;
  color: #fff;
}

Because class selectors are higher in specificity than element selectors and of the same specificity as attribute selectors, any rules applied in this way will override the element and attribute rules further up in the style sheet. So, my danger button will appear red with white text, but its other properties — like padding, the focus outline, and the margin applied via the flow algorithm — will remain intact.

<button class="danger">delete</button>

Name clashes may happen, occasionally, if several people are working on the same code base for a long time. But there are ways of avoiding this, like, oh, I don’t know, first doing a text search to check for the existence of the name you are about to take. You never know, someone may have solved the problem you’re addressing already.

Local Scope Utilities #

My favorite thing to do with utility classes is to set them on containers, then use this hook to affect the layout of child elements within. For example, I can quickly code up an evenly spaced, responsive, center-aligned layout for any elements:

.centered {
  text-align: center;
  margin-bottom: -1rem; /* adjusts for leftover bottom margin of children */
}
.centered > * {
  display: inline-block;
  margin: 0 0.5rem 1rem;
}
With this, I can center group list items, buttons, a combination of buttons and links, whatever. That’s thanks to the use of the > * part, which means that any immediate children of .centered will adopt these styles, in this scope, but inherit global and element styles, too.

And I’ve adjusted the margins so that the elements can wrap freely without breaking the vertical rhythm set using the * + * selector above it. It’s a small amount of code that provides a generic, responsive layout solution by setting a local scope for arbitrary elements.

My tiny (93B minified) flexbox-based grid system is essentially just a utility class like this one. It’s highly reusable, and because it employs flex-basis, no breakpoint intervention is needed. I just defer to flexbox’s wrapping algorithm.

.fukol-grid {
  display: flex;
  flex-wrap: wrap;
  margin: -0.5em; /* adjusting for gutters */
}
.fukol-grid > * {
  flex: 1 0 5em; /* The 5em part is the basis (ideal width) */
  margin: 0.5em; /* Half the gutter value */
}

Using BEM, you’d be encouraged to place an explicit “element” class on each grid item:

<div class="fukol"> <!-- the outer container, needed for vertical rhythm -->
  <ul class="fukol-grid">
    <li class="fukol-grid__item"></li>
    <li class="fukol-grid__item"></li>
    <li class="fukol-grid__item"></li>
    <li class="fukol-grid__item"></li>
  </ul>
</div>

But there’s no need. Only one identifier is required to instantiate the local scope. The items here are no more protected from outside influence than the ones in my version, targeted with > * — nor should they be. The only difference is the inflated markup.

So, now we’ve started incorporating classes, but only generically, as they were intended. We’re still not styling complex components independently. Instead, we’re solving system-wide problems in a reusable fashion. Naturally, you will need to document how these classes are used in your comments.

Utility classes like these take advantage of CSS’ global scope, the local scope, inheritance and the cascade simultaneously. The classes can be applied universally; they instantiate the local scope to affect just their child elements; they inherit styles not set here from the parent or global scope; and we’ve not overqualified using element or class selectors.

Here’s how our cascade looks now:

  1. global (html) styles and enforced inheritance,
  2. flow algorithm and exceptions (using the lobotomized owl selector),
  3. element and attribute styles,
  4. generic utility classes.

Of course, there may never be the need to write either of these example utilities. The point is that, if the need does emerge while working on one component, the solution should be made available to all components. Always be thinking in terms of the system.

Component-Specific Styles #

We’ve been styling components, and ways to combine components, from the beginning, so it’s tempting to leave this section blank. But it’s worth stating that any components not created from other components (right down to individual HTML elements) are necessarily over-prescribed. They are to components what IDs are to selectors and risk becoming anachronistic to the system.

In fact, a good exercise is to identify complex components (“molecules,” “organisms”) by ID only and try not to use those IDs in your CSS. For example, you could place #login on your log-in form component. You shouldn’t have to use #login in your CSS with the element, attribute and flow algorithm styles in place, although you might find yourself making one or two generic utility classes that can be used in other form components.

If you do use #login, it can only affect that component. It’s a reminder that you’ve moved away from developing a design system and towards the interminable occupation of merely pushing pixels.

Conclusion #

When I tell folks that I don’t use methodologies such as BEM or tools such as CSS modules, many assume I’m writing CSS like this:

header nav ul li {
  display: inline-block;
}
header nav ul li a {
  background: #008;
}

I don’t. A clear over-specification is present here, and one we should all be careful to avoid. It’s just that BEM (plus OOCSS, SMACSS, atomic CSS, etc.) are not the only ways to avoid convoluted, unmanageable CSS.

In an effort to defeat specificity woes, many methodologies defer almost exclusively to the class selector. The trouble is that this leads to a proliferation of classes: cryptic ciphers that bloat the markup and that — without careful attention to documentation — can confound developers new to the in-house naming system they constitute.

By using classes prolifically, you also maintain a styling system that is largely separate from your HTML system. This misappropriation of ‘separate concerns’ can lead to redundancy or, worse, can encourage inaccessibility: it’s possible to affect a visual style without affecting the accessible state along with it:

<input id="my-text" aria-invalid="false" class="text-input--invalid" />

In place of the extensive writing and prescription of classes, I looked at some other methods:

  • leveraging inheritance to set a precedent for consistency;
  • making the most of element and attribute selectors to support transparent, standards-based composition;
  • applying a code- and labor-saving flow layout system;
  • incorporating a modest set of highly generic utility classes to solve common layout problems affecting multiple elements.

All of these were put in service of creating a design system that should make writing new interface components easier and less reliant on adding new CSS code as a project matures. And this is possible not thanks to strict naming and encapsulation, but thanks to a distinct lack of it.

Even if you’re not comfortable using the specific techniques I’ve recommended here, I hope this article has at least gotten you to rethink what components are. They’re not things you create in isolation. Sometimes, in the case of standard HTML elements, they’re not things you create at all. The more you compose components from components, the more accessible and visually consistent your interface will be, and with less CSS to achieve that end.

There’s not much wrong with CSS. In fact, it’s remarkably good at letting you do a lot with a little. We’re just not taking advantage of that.