Structured Procrastination

Mike's Notes

Bravo!

This is kind of familiar. Go visit John's blog, it's cool.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Subscriptions > Amazing CTO
  • Home > Handbook > 

Last Updated

25/10/2025

Structured Procrastination

By: John Perry
Structured Procrastination: ?

John Perry is an emeritus professor of philosophy at Stanford. His office is Cordura 127 at CSLI, Stanford University, where he conducts several research projects. He is also a professor of philosophy (half-time) at the University of California, Riverside, on leave 2012–13.


Author practices jumping rope with seaweed while work awaits.

``. . . anyone can do any amount of work, provided it isn't the work he is supposed to be doing at that moment." -- Robert Benchley, in Chips off the Old Benchley, 1949

I have been intending to write this essay for months. Why am I finally doing it? Because I finally found some uncommitted time? Wrong. I have papers to grade, textbook orders to fill out, an NSF proposal to referee, dissertation drafts to read. I am working on this essay as a way of not doing all of those things. This is the essence of what I call structured procrastination, an amazing strategy I have discovered that converts procrastinators into effective human beings, respected and admired for all that they can accomplish and the good use they make of time. All procrastinators put off things they have to do. Structured procrastination is the art of making this bad trait work for you. The key idea is that procrastinating does not mean doing absolutely nothing. Procrastinators seldom do absolutely nothing; they do marginally useful things, like gardening or sharpening pencils or making a diagram of how they will reorganize their files when they get around to it. Why does the procrastinator do these things? Because they are a way of not doing something more important. If all the procrastinator had left to do was to sharpen some pencils, no force on earth could get him do it. However, the procrastinator can be motivated to do difficult, timely and important tasks, as long as these tasks are a way of not doing something more important.

Structured procrastination means shaping the structure of the tasks one has to do in a way that exploits this fact. The list of tasks one has in mind will be ordered by importance. Tasks that seem most urgent and important are on top. But there are also worthwhile tasks to perform lower down on the list. Doing these tasks becomes a way of not doing the things higher up on the list. With this sort of appropriate task structure, the procrastinator becomes a useful citizen. Indeed, the procrastinator can even acquire, as I have, a reputation for getting a lot done.

The most perfect situation for structured procrastination that I ever had was when my wife and I served as Resident Fellows in Soto House, a Stanford dormitory. In the evening, faced with papers to grade, lectures to prepare, committee work to be done, I would leave our cottage next to the dorm and go over to the lounge and play ping-pong with the residents, or talk over things with them in their rooms, or just sit there and read the paper. I got a reputation for being a terrific Resident Fellow, and one of the rare profs on campus who spent time with undergraduates and got to know them. What a set up: play ping pong as a way of not doing more important things, and get a reputation as Mr. Chips.

Procrastinators often follow exactly the wrong tack. They try to minimize their commitments, assuming that if they have only a few things to do, they will quit procrastinating and get them done. But this goes contrary to the basic nature of the procrastinator and destroys his most important source of motivation. The few tasks on his list will be by definition the most important, and the only way to avoid doing them will be to do nothing. This is a way to become a couch potato, not an effective human being.

At this point you may be asking, "How about the important tasks at the top of the list, that one never does?" Admittedly, there is a potential problem here.

The trick is to pick the right sorts of projects for the top of the list. The ideal sorts of things have two characteristics, First, they seem to have clear deadlines (but really don't). Second, they seem awfully important (but really aren't). Luckily, life abounds with such tasks. In universities the vast majority of tasks fall into this category, and I'm sure the same is true for most other large institutions. Take for example the item right at the top of my list right now. This is finishing an essay for a volume in the philosophy of language. It was supposed to be done eleven months ago. I have accomplished an enormous number of important things as a way of not working on it. A couple of months ago, bothered by guilt, I wrote a letter to the editor saying how sorry I was to be so late and expressing my good intentions to get to work. Writing the letter was, of course, a way of not working on the article. It turned out that I really wasn't much further behind schedule than anyone else. And how important is this article anyway? Not so important that at some point something that seems more important won't come along. Then I'll get to work on it.

Another example is book order forms. I write this in June. In October, I will teach a class on Epistemology. The book order forms are already overdue at the book store. It is easy to take this as an important task with a pressing deadline (for you non-procrastinators, I will observe that deadlines really start to press a week or two after they pass.) I get almost daily reminders from the department secretary, students sometimes ask me what we will be reading, and the unfilled order form sits right in the middle of my desk, right under the wrapping from the sandwich I ate last Wednesday. This task is near the top of my list; it bothers me, and motivates me to do other useful but superficially less important things. But in fact, the book store is plenty busy with forms already filed by non-procrastinators. I can get mine in mid-Summer and things will be fine. I just need to order popular well-known books from efficient publishers. I will accept some other, apparently more important, task sometime between now and, say, August 1st. Then my psyche will feel comfortable about filling out the order forms as a way of not doing this new task.

The observant reader may feel at this point that structured procrastination requires a certain amount of self-deception, since one is in effect constantly perpetrating a pyramid scheme on oneself. Exactly. One needs to be able to recognize and commit oneself to tasks with inflated importance and unreal deadlines, while making oneself feel that they are important and urgent. This is not a problem, because virtually all procrastinators have excellent self-deceptive skills also. And what could be more noble than using one character flaw to offset the bad effects of another?

ReBaz + Carpentries + RSE Conference

Mike's Notes

Plenty of opportunities in NZ to share research and upskill. I need to learn Python, and this might be a way to do so. I previously used Python with ESRI GIS, but that was 15 years ago.

Pipi 10 will have a Python interface.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library >
  • Home > Handbook > 

Last Updated

24/10/2025

ReBaz + Carpentries + RSE Conference

By: Mike Peters
On a Sandy Beach: 24/10/2025

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

Research Bazaar

"The annual ResBaz (Research Bazaar) event – an amazing free series of online webinars and workshops that bring together researchers from around the country to develop skills in digital tools and research practices."

The next ResBaz Aotearoa 2026: 29 June - 3 July.

The Carpentries

" ..The Carpentries is a non-profit organisation that teaches foundational coding and data science skills to researchers worldwide. There are numerous courses freely available, including an introduction to Python for library and information workers."

HPC Carpentries

"HPC Carpentry teaches HPC-oriented coding, and data science skills to researchers. We want to work towards bringing High Performance Computing under the Carpentries umbrella."

New Zealand Research Software Engineering Conference

"Within the research sector, there is a growing number of people who combine expertise in programming with an intricate understanding of research. Although this combination of skills is extremely valuable, these people lack a formal place in the academic system. This means there is no easy way to recognise their contribution, to reward them, or to represent their views.  

A community-driven event 

In June 2020, NeSI decided to rebrand its successful Science Coding Conference to be named the NZ Research Software Engineers (RSE) Conference. Motivation for the change is two-fold:

  • to include people from all research communities who work on the cusp of technical and research domains, and
  •  to more fully align with the goals of the Australia / New Zealand RSE community. 

Initiated in the UK, the RSE movement is a global phenomenon with many associations now set up around the world. In line with that global trend, New Zealand’s community of research software users and developers has steadily grown, with more roles and numbers of people at the intersection of software and research." - NZRSE

Putting a wrapper around Pipi

Mike's Notes

I had an epiphany while building out the new workspace UI yesterday.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library >
  • Home > Handbook > 

Last Updated

26/10/2025

Putting a wrapper around Pipi

By: Mike Peters
On a Sandy Beach: 23/10/2025

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

The new workspace applications smell like a wrapper around Pipi 9.

A GUI is a graphical user interface—windows, menus, tools, etc.— that you can click with a mouse. A wrapper is a piece of software that wraps an existing piece of software, like Pipi 9, and provides its interface (UI).

Headless

Pipi has been a "headless" working system since Pipi 8 in 2021. Getting Pipi to self-generate a UI has been a major task due to the number of rules and configuration standards imported. I wanted to solve some hard usability problems for users, which have taken time to address.

  • Accessibility
  • Localisation
  • Usability
  • Personalisation
  • Reliability
  • Security
  • Learning material

Documentation

I have also had to find a way to document a system with a novel architecture and subsystems that no one else uses. And then Pipi uses evolutionary algorithms rather than LLM, plus a lot of other stuff in the mix.

  • How to name things that are breaking conventions?
  • How to describe processes that are indeterminate yet highly reliable?
  • The UI will be a very thin skin on top of a multi-layer system that is at least 28 architecture layers deep.
  • There is no version control. Pipi is both the IDE and the source control. It's also the manager.

UI wrapper

It turns out that the new workspace Ui is simple, lightweight and very fast. It has a tiny attack surface, if at all. The UI looks, functions and smells like a wrapper. I figured that out only now while building the UI.

Discovery

I wonder what else will be discovered in this stretch. I have yet to start on the Templates, which are workspace customised configurations.

Example

  • Health
    • Modules
      • Booking
      • Facility
        • Cafe
        • Clean
        • Kitchen
        • Security
        • Ward
          • Room
          • Bed
      • Health Record
      • Health Worker
      • Imaging
      • Medication
      • Patient
      • Procedure
      • Transport
    • Settings
      • Billing
      • Dashboard
      • Plugin
      • Users
    • Templates
      • Allied
      • Animal Vet
      • Emergency
      • Family Doctor Clinic
      • Hospital
      • Public Health System
      • Patient

Thank you

I want to thank the early adopters who are helping me test and provide valuable feedback.

No Science, No Startups: The Innovation Engine We're Switching Off

Mike's Notes

A great description of how science works in the US.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Subscriptions > Steve Blank
  • Home > Handbook > 

Last Updated

22/10/2025

No Science, No Startups: The Innovation Engine We're Switching Off

By: Steve Blank
On a Sandy Beach: 13/10/2025

Blank created the Lean Launchpad class and I-Corps curriculum which became the standard for science commercialization for the National Science Foundation, the National Institutes of Health and the U.S. Department of Energy. As of 2023, more than 3,051 teams and 1,300 startups have employed Blank’s methodologies..

Blank is co-creator of the U.S. Department of Defense's[6] Hacking for Defense program, and served on the Defense Business Board and the U.S. Navy’s Science and Technological Board. He is co-creator of the Gordian Knot Center for National Security Innovation at Stanford University.

Tons of words have been written about the Trump Administrations war on Science in Universities. But few people have asked what, exactly, is science? How does it work? Who are the scientists? What do they do? And more importantly, why should anyone (outside of universities) care?

(Unfortunately, you won’t see answers to these questions in the general press – it’s not clickbait enough. Nor will you read about it in the science journals– it’s not technical enough. You won’t hear a succinct description from any of the universities under fire, either – they’ve long lost the ability to connect the value of their work to the day-to-day life of the general public.)

In this post I’m going to describe how science works, how science and engineering have worked together to build innovative startups and companies in the U.S.—and why you should care.

(In a previous post I described how the U.S. built a science and technology ecosystem and why investment in science is directly correlated with a country’s national power. I suggest you read it first.)

How Science Works

I was older than I care to admit when I finally understood the difference between a scientist, an engineer, an entrepreneur and a venture capitalist; and the role that each played in the creation of advancements that made our economy thrive, our defense strong and America great.

Scientists

Scientists (sometimes called researchers) are the people who ask lots of questions about why and how things work. They don’t know the answers. Scientists are driven by curiosity, willing to make educated guesses (the fancy word is hypotheses) and run experiments to test their guesses. Most of the time their hypotheses are wrong. But every time they’re right they move the human race forward. We get new medicines, cures for diseases, new consumer goods, better and cheaper foods, etc.

Scientists tend to specialize in one area – biology, medical research, physics, agriculture, computer science, materials, math, etc. — although a few move between areas. The U.S. government has supported scientific research at scale (read billions of $s) since 1940.

Scientists tend to fall into two categories: Theorists and Experimentalists.

Theorists

Theorists develop mathematical models, abstract frameworks, and hypotheses for how the universe works. They don’t run experiments themselves—instead, they propose new ideas or principles, explain existing experimental results, predict phenomena that haven’t been observed yet. Theorists help define what reality might be.

Theorists can be found in different fields of science. For example:

 Science Study 
Physics  Quantum field theory, string theory, quantum mechanics
Biology Neuroscience and cognition, Systems Biology, gene regulation
Chemistry  Molecular dynamics, Quantum chemistry
Computer Science Design algorithms, prove limits of computation
Economics  Build models of markets or decision-making
Mathematics  Causal inference, Bayesian networks, Deep Learning

The best-known 20th-century theorist was Albert Einstein. His tools were a chalkboard and his brain. in 1905 he wrote an equation E=MC2 which told the world that a small amount of mass can be converted into a tremendous amount of energy. When he wrote it down, it was just theory. Other theorists in the 1930s and ’40s took Einstein’s theory and provided the impetus for building the atomic bomb. (Leo Szilard conceived neutron chain reaction idea, Hans Bethe led the Theoretical Division at Los Alamos, Edward Teller developed hydrogen bomb theory.) Einstein’s theory was demonstrably proved correct over Hiroshima and Nagasaki.

Experimentalists

In addition to theorists, other scientists – called experimentalists – design and run experiments in a lab. The pictures you see of scientists in lab coats in front of microscopes, test tubes, particle accelerators or NASA spacecraft are likely experimentalists. They test hypotheses by developing and performing experiments. An example of this would be NASA’s James Webb telescope or the LIGO Gravitational-Wave Observatory experiment. (As we’ll see later, often it’s engineers who build the devices the experimentalists use.)

Some of these experimentalists focus on Basic Science, working to get knowledge for its own sake and understand fundamental principles of nature with no immediate practical use in mind.

Other experimentalists work in Applied Science, which uses the findings and theories derived from Basic Science to design, innovate, and improve products and processes.

Applied scientists solve practical problems oriented toward real-world applications. (Scientists at Los Alamos weretrying to understand the critical mass of U-235 (the minimum amount that would explode.) Basic science lays the groundwork for breakthroughs in applied science. For instance: Quantum mechanics (basic science) led to semiconductors which led to computers (applied science). Germ theory (basic science) led to antibiotics and vaccines (applied science). In the 20th century Applied scientists did not start the companies that make end products. Engineers and entrepreneurs did this. (In the 21st century more Applied Scientists, particularly in life sciences, have also spun out companies from their labs.)

Scientists

Where is Science in the U.S. Done?

America’s unique insight that has allowed it to dominate Science and invention, is that after WWII we gave Research and Development money to universities, rather than only funding government laboratories. No other country did this at scale.

Corporate Research Centers

In the 20th century, U.S. companies put their excess profits into corporate research labs. Basic research in the U.S. was done in at Dupont, Bell Labs, IBM, AT&T, Xerox, Kodak, GE, et al.

This changed in 1982, when the Securities and Exchange Commission ruled that it was legal for companies to buy their own stock (reducing the number of shares available to the public and inflating their stock price.) Very quickly Basic Science in corporate research all but disappeared. Companies focused on Applied Research to maximize shareholder value. In its place, Theory and Basic research is now done in research universities.

Research Universities

From the outside (or if you’re an undergraduate) universities look like a place where students take classes and get a degree. However, in a research university there is something equally important going on. Science faculty in these schools not only teach, but they are expected to produce new knowledge—through experiments, publications, patents, or creative work. Professors get grants and contracts from federal agencies (e.g., NSF, NIH, DoD), foundations, and industry. And the university builds Labs, centers, libraries, and advanced computing facilities that support these activities.

In the U.S. there are 542 research universities, ranked by the Carnegie Classification into three categories.

R1: 187 Universities – Very High Research Activity

  • Conduct extensive research and award many doctoral degrees.
  • Examples: Stanford, UC Berkeley, Harvard, MIT, Michigan, Texas A&M …

R2: 139 Universities – High Research Activity

  • Substantial but smaller research scale.
  • Examples: Baylor, Wake Forest, UC Santa Cruz, …

R3: 216 Research Colleges/Universities

  • Limited research focus; more teaching-oriented doctoral programs.
  • Smaller state universities

Why Universities Matter to Science

U.S. universities perform about 50% of all basic science research (physics, chemistry, biology, social sciences, etc.) because they are training grounds for graduate students and postdocs. Universities spend ~$109 billion a year on research. ~$60 billion of that $109 billion comes from the National Institutes for Health (NIH) for biomedical research, National Science Foundation (NSF) for basic science, Department of War (DoW), Department of Energy (DOE), for energy/physics/nuclear, DARPA, NASA. (Companies tend to invest in applied research and development, that leads directly to saleable products.)

Professors (especially in Science, Technology, Engineering and Math) run labs that function like mini startups. They ask research questions, then hire grad students, postdocs, and staff and write grant proposals to fund their work, often spending 30–50% of their time writing and managing grants. When they get a grant the lead researcher (typically a faculty member/head of the lab) is called the Principal Investigator (PI).

The Labs are both workplaces and classrooms. Graduate students and Postdocs do the day-to-day science work as part of their training (often for a Ph.D.). Postdocs are full-time researchers gaining further specialization. Undergraduates may also assist in research, especially at top-tier schools.

(Up until 2025, U.S. science was deeply international with ~40–50% of U.S. basic research done by foreign-born researchers (graduate students, postdocs, and faculty). Immigration and student visas were a critical part of American research capacity.)

The results of this research are shared with the agencies that funded it, published in journals, presented at conferences and often patented or spun off into startups via technology transfer offices. A lot of commercial tech—from Google search to CRISPR—started in university labs.

Universities support their science researchers with basic administrative staff (for compliance, purchasing, and safety) but uniquely in the U.S., by providing the best research facilities (labs, cleanrooms, telescopes), and core scientific services: DNA sequencing centers, electron microscopes, access to cloud, data analysis hubs, etc. These were the best in the world – until the sweeping cuts in 2025.

Engineers Build on the Work of Scientists

Engineers design and build things on top of the discoveries of scientists. For example, seven years after scientists split the atom, it took 10s of thousands of engineers to build an atomic bomb. From the outset, the engineers knew what they wanted to build because of the basic and applied scientific research that came before them.

Scientists Versus Engineers

Engineers create plans, use software to test their designs, then… cut sheet metal, build rocket engines, construct buildings and bridges, design chips, build equipment for experimentalists, design cars, etc.

As an example, at Nvidia their GPU chips are built in a chip factory (TSMC) using the Applied science done by companies like Applied Materials which in turn is based on Basic science of semiconductor researchers. And the massive data centers OpenAI, Microsoft, Google, et al that use Nvidia chips are being built by mechanical and other types of engineers.

My favorite example is that the reusable SpaceX rocket landings are made possible by the Applied Science research on Convex Optimization frameworks and algorithms by Steven Boyd of Stanford. And Boyd’s work was based on the Basic science mathematical field of convex analysis (SpaceX, NASA, JPL, Blue Origin, Rocket Lab all use variations of Convex Optimization for guidance, control, and landing.)

Startup Entrepreneurs Build Iteratively and Incrementally

Entrepreneurs build companies to bring new products to market. They hire engineers to build, test and refine products.

Engineers and entrepreneurs operate with very different mindsets, goals, and tolerances for risk and failure. (Many great entrepreneurs start as engineers e.g., Musk, Gates, Page/Brin). An engineer’s goal is to design and deliver a solution to a known problem with a given set of specifications.

In contrast, entrepreneurs start with a series of unknowns about who are the customers, what are the wanted product features, pricing, etc. They retire each of these risks by building an iterative series of minimum viable products to find product/market fit and customer adoption. They pivot their solution as needed when they discover their initial assumptions are incorrect. (Treating each business unknown as a hypothesis is the entrepreneurs’ version of the Scientific Method.)

Venture Capitalists Fund Entrepreneurs

Venture capitalists (VCs) are the people who fund entrepreneurs who work with engineers who build things that applied scientists have proven from basic researchers.

Unlike banks which will give out loans for projects that have known specifications and outcomes, VCs invest in a portfolio of much riskier investments. While banks make money on the interest they charge on each loan, VCs take part ownership (equity) in the companies they invest in. While most VC investments fail, the ones that succeed make up for that.

Most VCs are not scientists. Few are engineers, some have been entrepreneurs. The best VCs understand technical trends and their investments help shape the future. VCs do not invest in science/researchers. VCs want to minimize the risk of their investment, so they mostly want to take engineering and manufacturing risk, but less so on applied science risk and rarely on basic research risk. Hence the role of government and Universities.

VCs invest in projects that can take advantage of science and deliver products within the time horizon of their funds (3–7 years). Science often needs decades before a killer app is visible.

As the flow of science-based technologies dries up, the opportunities for U.S. venture capital based on deep tech will decline, with its future in countries that are investing in science – China or Europe.

Why Have Scientists? Why Not Just a Country of Engineers, Entrepreneurs and VCs (or AI)?

If you’ve read so far, you might be scratching your head and asking, “Why do we have scientists at all? Why pay for people to sit around and think? Why spend money on people who run experiments when most of those experiments fail? Can’t we replace them with AI?”

The output of this university-industry-government science partnership became the foundation of Silicon Valley, the aerospace sector, the biotechnology industry, Quantum and AI. These investments gave us rockets, cures for cancer, medical devices, the Internet, Chat GPT, AI and more.

Investment in science is directly correlated with national power. Weaken science, you weaken the long-term growth of the economy, and national defense.

Tech firms’ investments of $100s of billions in AI data centers is greater than the federal government’s R&D expenditures. But these investments are in engineering not in science. The goal of making scientists redundant using artificial general intelligence misses the point that AI will (and is) making scientists more productive – not replacing them.

Countries that neglect science become dependent on those that don’t. U.S. post-WWII dominance came from basic science investments (OSRD, NSF, NIH, DOE labs). After WWII ended, the UK slashed science investment which allowed the U.S. to commercialize the British inventions made during the war.

The Soviet Union’s collapse partly reflected failure to convert science into sustained innovation, during the same time that U.S. universities, startups and venture capital created Silicon Valley. Long-term military and economic advantage (nuclear weapons, GPS, AI) trace back to scientific research ecosystems.

Lessons Learned

  • Scientists come in two categories
    • Theorists and experimentalists
    • Two types of experimentalists; Basic science (learn new things) or applied science (practical applications of the science)
    • Scientists train talent, create patentable inventions and solutions for national defense
  • Engineers design and build things on top of the discoveries of scientists
  • Entrepreneurs test and push the boundaries of what products could be built
  • Venture Capital provides the money to startups
  • Scientists, engineers, entrepreneurs – these roles are complementary
    • Remove one and the system degrades
  • Science won’t stop
    • Cut U.S. funding, then science will happen in other countries that understand its relationship to making a nation great – like China.
    • National power is derived from investments in Science
    • Reducing investment in basic and applied science makes America weak

 Appendix – How Does Science Work? – The Scientific Method

Whether you were a theorist or experimentalist, for the last 500 years the way to test science was by using the scientific method. This method starts by a scientist wondering and asking, “Here’s how I think this should work, let’s test the idea.”

The goal of the scientific method is to turn a guess (in science called a hypothesis) into actual evidence. Scientists do this by first designing an experiment to test their guess/hypothesis. They then run the experiment and collect and analyze the result and ask, “Did the result validate, invalidate the hypothesis? Or did it give us completely new ideas?” Scientists build instruments and run experiments not because of what they know, but because of what they don’t know.

These experiments can be simple ones costing thousands of dollars that can be run in a university biology lab while others may require billions of dollars to build a satellite, particle accelerator or telescope. (The U.S. took the lead in Science after WWII when the government realised that funding scientists was good for the American economy and defence.)

Good science is reproducible. Scientists just don’t publish their results, but they also publish the details of how they ran their experiment. That allows other scientists to run the same experiment and see if they get the same result for themselves. That makes the scientific method self-correcting (you or others can see mistakes).

One other benefit of the scientific method is that scientists (and the people who fund them) expect most of the experiments to fail, but the failures are part of learning and discovery. They teach us what works and what doesn’t. Failure in science testing unknowns means learning and discovery.

You no longer need JavaScript

Mike's Notes

I discovered this blog article by Estonian Lyra Rebane via Ray Camden. The original article has much more to play with.

Thanks, Ray, for spotting.

Ray wrote

"I feel like this is something that's been shared before, and heck, I've talked about this myself many times as well, but it's a useful reminder that many things we've used JavaScript for in the past are not actually necessary and can be done by simpler, less complex means. Check out "You no longer need JavaScript", a great post focused on CSS improvements primarily but also some relevant HTML features you may not be aware of."

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Subscriptions > Ray Camden Blog
  • Home > Handbook > 

Last Updated

21/10/2025

You no longer need JavaScript

By: Lyra Rebane
Lyra Horse Blog: 28/08/2025

hi! i’m lyra and this is my epic blog where i post epic articles about infosec, programming, and other cool stuff! There’s also stuff on my home page.

So much of the web these days is ruined by the bloat that is modern JavaScript frameworks. React apps that take several seconds to load. NextJS sites that throw random hydration errors. The node_modules folder that takes up gigabytes on your hard drive.

It’s awful. And you don’t need it.

Name Status Type Size Time
app 200 document 153.8 kB 51 ms
6920616d20612066-s.p.6f6e7421.woff2 200 font 31.5 kB 32 ms
686579206d652074-s.p.6f6f2121.woff2 200 font 28.5 kB 116 ms
77687920646f6573.css 200 stylesheet 253 kB 47 ms
2074686520646566.js 200 script 648 kB 83 ms
61756c74206e6578.js 200 script 166 kB 363 ms
746a732074616b65.js 200 script 83.3 kB 46 ms
turbopack-20757020302e354d.js 200 script 38.0 kB 95 ms
423f207468617427.js 200 script 414 B 34 ms
73206d6f72652074.js 200 script 32.6 kB 49 ms
68616e206d792065.js 200 script 15.1 kB 71 ms
6e7469726520626c.js 200 script 143 kB 48 ms
6f6721 hey there! 200 script 4.1 kB 103 ms

The intro paragraph of this post is tongue-in-cheek. It’s there to get you to read the rest of the post. I suspect the megabytes of tracking scripts intertwined with bad code is far more likely to be the real culprit behind all the terrible sites out there. Web frameworks have their time and place. And despite my personal distaste for them, I know they are used by many teams to build awesome well-optimized apps.

Despite that, I think there’s some beauty in leaving it all behind. Not just the frameworks, but JavaScript altogether. Not every site needs JavaScript. Perhaps your e-commerce site needs it for its complex carts and data visualization dashboards, but is it really a necessity for most of what’s out there?

It’s actually pretty incredible what HTML and CSS alone can achieve.

So, what do you say?

My goal with this article is to share my perspectives on the web, as well as introduce many aspects of modern HTML/CSS you may not be familiar with. I’m not trying to make you give up JavaScript, I’m just trying to show you everything that’s possible, leaving it up to you to pick what works best for whatever you’re working on.

I think there’s a lot most web developers don’t know about CSS.

And I think JS is often used where better alternatives exist.

So, let me show you what’s out there.

“But CSS sucks”

I believe a lot of the negativity towards CSS stems from not really knowing how to use it. Many developers kind of just skip learning the CSS fundamentals in favor of the more interesting Java- and TypeScript, and then go on to complain about a styling language they don’t understand.

I suspect this is due to many treating CSS as this silly third wheel for adding borders and box-shadows to a webapp. It’s undervalued and often compared to glorified crayons, rather than what it really is - a powerful domain-specific programming language.

It’s telling when to this day the only CSS joke in the webdev circles is centering a div.

i am a div
body {
  display: flex;
flex-direction: row
flex-wrap: nowrap
align-content: normal
justify-content: center
align-items: center
  justify-content: center;
  align-items: center;
}

Yes, the syntax isn’t the prettiest, but is it really that hard?

Besides, your devtools probably [1] come with a fun little gadget that lets you fiddle with the flexbox by just clicking around. You don’t even need to remember the syntax.

I don’t think CSS is fundamentally any more difficult than JS, but if you skip the basics on one and only focus on the other, it’s no surprise it feels that way.

“But it’s painful to write”

Another source of disdain for CSS is how awful it has been to write in the past. This is very much true, and is probably why things like Sass and Tailwind [2] exist.

But that’s the thing, it used to be bad.

btw u should write css like

cool-thing {
    display: flex;
    &[shadow] {
        box-shadow: 1px 1px #0007;
    }
    @media (width < 480px) {
        flex-direction: column;
    }
}

and html like

<cool-thing shadow>wow</cool-thing>

because it's allowed & modern & neat!

(yes! the code above is standards compliant [3])

In the past few years, CSS has received a ton of awesome quality-of-life additions, making it nice to do stuff that has historically required preprocessors or JavaScript.

Nesting is definitely one of my favorite additions!

In the past, you’ve had to write code that looks like this:

:root {
  --like-color: #24A4F3;
  --like-color-hover: #54B8F5;
  --like-color-active: #0A6BA8;
}
.post {
  display: block;
  background: #EEE;
  color: #111;
}
.post .avatar {
  width: 48px;
  height: 48px;
}
.post > .buttons {
  display: flex;
}
.post > .buttons .label {
  font-size: 24px;
  padding: 8px;
}
.post > .buttons .like {
  cursor: pointer;
  color: var(--like-color);
}
.post > .buttons .like:hover {
  color: var(--like-color-hover);
}
.post > .buttons .like:active {
  color: var(--like-color-active);
}
@media screen (max-width: 800px) {
  .post > .buttons .label {
    font-size: 16px;
    padding: 4px;
  }
}
@media (prefers-color-scheme: dark) {
  .post {
    background: #222;
    color: #FFF;
  }
}

And yeah, that’s pretty awful to work with. For anything that involves multiple chained selectors, you kind of have to keep a mental map of how every parent selector relates to its children, and the more CSS you add the harder it gets.

But let’s try it with nesting:

:root {
  --like-color: #24A4F3;
  --like-color-hover: hsl(from var(--like-color) h s calc(l + 10));
  --like-color-active: hsl(from var(--like-color) h s calc(l - 20));
}
.post {
  display: block;
  background: #EEE;
  color: #111;
  @media (prefers-color-scheme: dark) {
    background: #222;
    color: #FFF;
  }
  .avatar {
    width: 48px;
    height: 48px;
  }
  & > .buttons {
    display: flex;
    .label {
      font-size: 24px;
      padding: 8px;
      @media (width <= 800px) {
        font-size: 16px;
        padding: 4px;
      }
    }
    .like {
      cursor: pointer;
      color: var(--like-color);
      &:hover { color: var(--like-color-hover); }
      &:active { color: var(--like-color-active); }
    }
  }
}

That is way nicer to read [4]! All the relevant parts are right next to each other, so it’s a lot easier to understand what’s going on. Seeing the &:hover and &:active right next to the .like button is especially nice imo.

And since you can sort of see the structure - the parent selectors “guarding” the child ones - it also makes it a lot easier to get away with short and simple class names (or even referring to elements themselves).

You may have noticed that I’m also making use of relative colors in the second example. I think the MDN article has a lot of awesome examples, but the jist of it is that you can take an existing color, modify it in many different ways across multiple color spaces, and mix it with other colors using color-mix().

[IMAGE]

(yes! the color picker above is written in just css)

The source

/* remove blue from a color */
rgb(from #123456 r g 0);
/* make a color transparent */
rgb(from #123456 r g b / 0.5);
/* make a color lighter */
hsl(from #123456 h s calc(l + 10));
/* change the hue in oklch color space */
oklch(from #123456 l c calc(h + 10));
/* mix two colors in oklab color space */
color-mix(in oklab, #8CFFDB, #04593B 25%);
These snippets are really useful for when you want something to be just ever so slightly darker or brighter, such as a button hover effect or a matching border color, and they’re way nicer to use than doing all those color conversions in JavaScript. If you’re feeling particularly adventurous, you could even go ahead and generate your entire color scheme in just CSS.
/* This is editable ^_^ */
@property --cqw {
  syntax: '<length>';
  inherits: true;
  initial-value: 1cqw;
}
@property --cqh {
  syntax: '<length>';
  inherits: true;
  initial-value: 1cqh;
}
@property --vw {
  syntax: '<length>';
  inherits: true;
  initial-value: 100vw;
}
@property --vh {
  syntax: '<length>';
  inherits: true;
  initial-value: 100vh;
}
@property --svh {
  syntax: '<length>';
  inherits: true;
  initial-value: 100svh;
}
@property --dvh {
  syntax: '<length>';
  inherits: true;
  initial-value: 100dvh;
}
@property --lvh {
  syntax: '<length>';
  inherits: true;
  initial-value: 100lvh;
}
/* oklch easter egg */
color-demo:has(color-style:active):not(:has(color-swatch:active)) {
  color-bg {
    background-image: linear-gradient(in oklch to right,
      oklch(75% 100% 0),
      oklch(75% 100% 120deg),
      oklch(75% 100% 240deg),
      oklch(75% 100% 360deg)
    ), linear-gradient(#000, #FFF);
  }
  color-result>div {
    --picked-color: oklch(calc(tan(atan2(var(--cqh), 1px)) / calc(var(--h) / 200)) 100% calc(tan(atan2(var(--cqw), 1px)) / calc(var(--w) / 720)));
  }
}
color-demo, color-picker, color-point, color-result, color-bg {
  display: block;
}
color-demo {
  display: grid;
  grid-template-rows: 1fr;
  grid-template-columns: 1fr;
  container-type: size;
  /*--w: 720;*/
  /*--h: 400;*/
  --w: min(calc(tan(atan2(var(--vw), 1px)) - 48), 768);
  --h: 300;
  width: calc(var(--w) * 1px);
  height:calc(var(--h) * 1px + 324px);
  border-radius: 6px;
  background: #FFF;
  overflow: clip;
  /*
  background:  linear-gradient(in hsl to right,
    hsl(0 100% 50%),
    hsl(120deg 100% 50%),
    hsl(240deg 100% 50%),
    hsl(360deg 100% 50%)
  ), linear-gradient(#000, #FFF);
  background-size: calc(100% - 16px) calc(100% - 16px);
  background-position: center;
  background-repeat: no-repeat;
  background-blend-mode: overlay;
  */
  position: relative;
  color-bg {
    color-scheme: only light;
    grid-area: 1 / 1;
    width: calc(100% - 16px);
    height: calc(var(--h) * 1px - 16px);
    margin: 8px;
    border-radius: 4px;
    background:  linear-gradient(in hsl to right,
      hsl(0 100% 50%),
      hsl(120deg 100% 50%),
      hsl(240deg 100% 50%),
      hsl(360deg 100% 50%)
    ), linear-gradient(#000, #FFF);
    background-position: center;
    background-repeat: no-repeat;
    background-blend-mode: overlay;
    box-shadow: 1px 1px 5px 0 #0002;
  }
  color-picker {
    color-scheme: only light;
    display: grid;
    grid-area: 1 / 1;
    position: relative;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    width:fit-content;
    height:fit-content;
    &>*{
      grid-column: 1; grid-row: 1;
    }
    color-indicator {
      --sw1: 0;
      --sw2: 0;
      --sw3: 0;
      --sw4: 0;
      --sw5: 0;
      --sw6: 0;
      --sw7: 0;
      --sw8: 0;
      --sw9: 0;
    }
    &:has(>color-result color-swatch > :nth-child(1):hover) color-indicator { --sw1: 2px }
    &:has(>color-result color-swatch > :nth-child(2):hover) color-indicator { --sw2: 2px }
    &:has(>color-result color-swatch > :nth-child(3):hover) color-indicator { --sw3: 2px }
    &:has(>color-result color-swatch > :nth-child(4):hover) color-indicator { --sw4: 2px }
    &:has(>color-result color-swatch > :nth-child(5):hover) color-indicator { --sw5: 2px }
    &:has(>color-result color-swatch > :nth-child(6):hover) color-indicator { --sw6: 2px }
    &:has(>color-result color-swatch > :nth-child(7):hover) color-indicator { --sw7: 2px }
    &:has(>color-result color-swatch > :nth-child(8):hover) color-indicator { --sw8: 2px }
    &:has(>color-result color-swatch > :nth-child(9):hover) color-indicator { --sw9: 2px }
    &:has(>color-result color-swatch[analogous]:hover) color-indicator {
      /*filter: drop-shadow(37px 0px 0px #EEE) drop-shadow(0px 20px 0px #EEE);*/
      box-shadow: 0 0 1px 1px #0005 inset, 0 0 0px 1px #0008 inset,
                  0 0 0 var(--sw3) #000,
                  calc(var(--w) * -.111px) 0px 0px var(--sw1) #A4F9,
                  calc(var(--w) * -.055px) 0px 0px var(--sw2) #C2F9,
                  calc(var(--w) * 0.055px) 0px 0px var(--sw4) #F2C9,
                  calc(var(--w) * 0.111px) 0px 0px var(--sw5) #F4A9;
    }
    &:has(>color-result color-swatch[primary]:hover) color-indicator {
      box-shadow: 0 0 1px 1px #0005 inset, 0 0 0px 1px #0008 inset,
                  0 0 0 var(--sw2) #000,
                  calc(var(--w) * -.5px) 0px 0px var(--sw1) #3039,
                  0 0 0 #0000,
                  0 0 0 #0000,
                  calc(var(--w) * 0.5px) 0px 0px var(--sw3) #3039;
    }
    &:has(>color-result color-swatch[monochrome]:hover) color-indicator {
      bottom: calc(100% - 14px - var(--h) * 0.1px);
      outline: 1px solid #0000;
      box-shadow: 0 0 1px 1px #0005 inset, 0 0 0px 1px #0008 inset,
                  0 0 0 var(--sw1) #000,
                  0 calc(var(--h) * 0.1px) 0 var(--sw2) #F6F9,
                  0 calc(var(--h) * 0.2px) 0 var(--sw3) #E5E9,
                  0 calc(var(--h) * 0.3px) 0 var(--sw4) #C4C9,
                  0 calc(var(--h) * 0.4px) 0 var(--sw5) #A3A9,
                  0 calc(var(--h) * 0.5px) 0 var(--sw6) #8289,
                  0 calc(var(--h) * 0.6px) 0 var(--sw7) #4149,
                  0 calc(var(--h) * 0.7px) 0 var(--sw8) #2029,
                  0 calc(var(--h) * 0.8px) 0 var(--sw9) #0009;
    }
    &:has(>color-result color-swatch[success]:hover) color-indicator {
      bottom: calc(100% - 14px - var(--h) * 0.48px);
      right: calc(100% - 14px - var(--w) * 0.0166px);
      outline: 1px solid #0000;
      box-shadow: 0 0 1px 1px #0005 inset, 0 0 0px 1px #0008 inset,
                  0 0 0 var(--sw1) #000,
                  calc(var(--w) * 0.117px) 0 0 var(--sw2) #5529,
                  calc(var(--w) * 0.372px) 0 0 var(--sw3) #2A29,
                  calc(var(--w) * 0.544px) 0 0 var(--sw4) #2289,
                  calc(var(--w) * 0.711px) 0 0 var(--sw5) #0000;
    }
  }
  color-point {
    width: 16px;
    height: 16px;
    background: #0003;
    resize: both;
    overflow: auto;
    max-width: 100cqw;
    max-height: calc(var(--h) * 1px);
    opacity: 0;
    clip-path: polygon(calc(100% - 16px) calc(100% - 16px), calc(100% - 16px) 100%, 100% 100%, 100% calc(100% - 16px));
    /* for mobile support */
    touch-action: none;
  }
  color-indicator {
    display: block;
    position: absolute;
    width: 8px;
    height: 8px;
    border-radius: 16px;
    pointer-events: none;
    border: 2px solid #FFF;
    outline: 1px solid #000;
    box-shadow: 0 0 1px 1px #0005 inset, 0 0 0px 1px #0008 inset;
    transition: box-shadow 0.5s, bottom 0.5s, right 0.5s, outline 0.5s;
    right: 2px;
    bottom: 2px;
    /* make it easier to grab the thing on mobile */
    @media (pointer: coarse) {
      right: -2px;
      bottom: -2px;
    }
  }
  color-result {
    container-type: size;
    width: 100%;
    pointer-events: none;
    &>div {
      --cqw: calc(50cqw);
      --cqh: calc(50cqh);
      --picked-color: hsl(calc(tan(atan2(var(--cqw), 1px)) / calc(var(--w) / 720)) 100% calc(tan(atan2(var(--cqh), 1px)) / calc(var(--h) / 200)));
      --picked-color-hue: calc(tan(atan2(var(--cqw), 1px)) / calc(var(--w) / 720));
      display: flex;
      position: absolute;
      width: calc(var(--w) * 1px);
      height: 400px;
      top: calc(var(--h) * 1px);
    }
  }
  color-style {
    color-scheme: initial;
    /*--background: lch(from var(--picked-color) calc((1 - round(l / 128)) * 255) c h);*/
    --primary: var(--picked-color);
    --primary-contrast: lch(from var(--primary) calc((1 - round(l / 128)) * 255) 0 0);
    --secondary: lch(from var(--primary) l calc(min(255 - c * 10,148 - 100*round(l/100))) var(--picked-color-hue));
    --secondary-contrast: lch(from var(--secondary) calc((1 - round(l / 128)) * 255) 0 0);
    --complimentary: lch(from var(--primary) l c calc(h + 180));
    --complimentary-contrast: lch(from var(--complimentary) calc((1 - round(l / 128)) * 255) 0 0);
    --analogous-a: hsl(from var(--primary) calc(h - 40) s l);
    --analogous-b: hsl(from var(--primary) calc(h - 20) s l);
    --analogous-c: hsl(from var(--primary) calc(h + 20) s l);
    --analogous-d: hsl(from var(--primary) calc(h + 40) s l);
    --success: hsl(from var(--primary) 140deg 70 50%);
    --danger: hsl(from var(--primary) 6deg 76 50%);
    --warning: hsl(from var(--primary) 48deg 83 50%);
    --info: hsl(from var(--primary) 202deg 70 50%);
    --monochrome-100: lch(from var(--primary) 10% c h);
    --monochrome-200: lch(from var(--primary) 20% c h);
    --monochrome-300: lch(from var(--primary) 30% c h);
    --monochrome-400: lch(from var(--primary) 40% c h);
    --monochrome-500: lch(from var(--primary) 50% c h);
    --monochrome-600: lch(from var(--primary) 60% c h);
    --monochrome-700: lch(from var(--primary) 70% c h);
    --monochrome-800: lch(from var(--primary) 80% c h);
    --monochrome-900: lch(from var(--primary) 90% c h);
    --background: lch(from var(--primary) 100% calc(c / 5) h);
    /* https://github.com/system-fonts/modern-font-stacks?tab=readme-ov-file#geometric-humanist */
    font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
    pointer-events: all;
    border: 1px solid color-mix(in hsl, var(--primary), #4444);
    background: var(--background);
    border-radius: 4px;
    margin: 8px;
    display: block;
    height: fit-content;
    width: 100%;
    @media (width < 480px) {
      font-size: 75%;
    }
    @media (width < 360px) {
      font-size: 50%;
    }
    &:not(:has(>color-swatch>div:active)) > color-swatch > div:hover {
      padding-left: 64px;
    }
    color-swatch {
      color-scheme: only light;
      margin: 10px;
      border-radius: 4px;
      overflow: clip;
      color: #000;
      width: calc(100% - 20px);
      height: 64px;
      display: flex;
      box-shadow: 1px 1px 5px 0 #0004;
      >div {
        flex-grow: 1;
        padding: 4px;
        font-weight: 600;
        display: flex;
        justify-content: flex-end;
        align-items: flex-end;
        -webkit-user-select: none;
        user-select: none;
        transition: padding 0.4s, box-shadow 0.4s, scale 0.4s;
        cursor: grab;
        box-shadow: none;
        &:active {
          cursor: grabbing;
          /*padding-left: 32px;*/
          padding-left: 52px;
          box-shadow: 0 0 16px #0005;
          z-index: 10;
        }
        &:last-child {
          margin-right: -1px;
        }
      }
      &[success] {
        color: #FFF;
        >:nth-child(1) { background: var(--success); color: var(--success-contrast); }
        >:nth-child(2) { background: var(--danger); color: var(--danger-contrast); }
        >:nth-child(3) { background: var(--warning); color: var(--warning-contrast); }
        >:nth-child(4) { background: var(--info); color: var(--info-contrast); }
      }
      &[primary] {
        >:nth-child(1) { background: var(--primary); color: var(--primary-contrast); }
        >:nth-child(2) { background: var(--complimentary); color: var(--complimentary-contrast); }
        >:nth-child(3) { background: var(--secondary); color: var(--secondary-contrast); }
      }
      &[monochrome] {
        >:nth-child(-n+5) { color: #FFF; }
        >:nth-child(1) { background: var(--monochrome-100); }
        >:nth-child(2) { background: var(--monochrome-200); }
        >:nth-child(3) { background: var(--monochrome-300); }
        >:nth-child(4) { background: var(--monochrome-400); }
        >:nth-child(5) { background: var(--monochrome-500); }
        >:nth-child(6) { background: var(--monochrome-600); }
        >:nth-child(7) { background: var(--monochrome-700); }
        >:nth-child(8) { background: var(--monochrome-800); }
        >:nth-child(9) { background: var(--monochrome-900); }
      }
      &[analogous] {
        color: var(--primary-contrast);
        >:nth-child(1) { background: var(--analogous-a); }
        >:nth-child(2) { background: var(--analogous-b); }
        >:nth-child(3) { background: var(--primary); }
        >:nth-child(4) { background: var(--analogous-c); }
        >:nth-child(5) { background: var(--analogous-d); }
      }
    }
  }
}

There are so many cool new CSS features that make writing it just that little bit nicer. Things like letting you use (width <= 768px) instead of (max-width: 768px) in your @media query, the lh unit that matches the line-height, the scrollbar-gutter property that solves the little scrollbar-related layout shifts, or the ability to finally center stuff vertically without flex/grid.

And all of this is brought together by the cherry on top that is Baseline. It’s a guarantee that a specific feature works in every major browser [5], and it also lets you know since when - newly available features work in all the latest browsers, and widely available ones work in browsers up to 2.5 years old. Nesting, for example, has been fully supported in all browsers since December 2023, and thus will become widely available in June 2026. You can find the Baseline symbols in various places, such as the MDN docs [6].

These are just a few examples of what makes modern CSS so much nicer to write than what we had even just 5 years ago. It almost feels like comparing ES3 [7] to ECMAScript 2025 - and I wouldn’t blame your grudge if the former is what you’re used to.

Why bother?

Okay, so CSS has more quality-of-life stuff than before. Still, why would one choose to use it over something else? Doesn’t JavaScript already let us do everything just fine?

You need to disable JavaScript to run this app.

I think my reasons for using CSS fall into two main categories - because some users don’t want to use JavaScript, and because doing things in CSS can be genuinely better.

My blog, for example, focuses on infosec topics. Many security researchers (myself included) use a hardened browser configuration to protect themselves, which often means disabling JavaScript by default. I think it’s nice that they can fully experience my blog without changing their security settings or running a separate, sandboxed browser.

The same goes for privacy-conscious users, and it makes sense! As an experiment, I opened up a local Estonian news site in a web browser with JavaScript enabled. Can you guess how many js files it fetched? (answer in footnote [8]) That’s crazy! You do not want that running on your computer.

But surely, you are not one of the evil devs who loads a double-digit number of analytics scripts on your site - is there still any reason to reach for CSS?

Well, I think a lot of things are just plain nicer to make in HTML/CSS, both from the developer and end-user perspectives, be it for ease of use, accessibility, or performance.

Hover effects for your buttons? Toast animations? Input validation? All of these things just work in CSS, and you won’t have to reinvent the wheel, or throw kilobytes of someone else’s code at it. There will always be some cases where you do need that extra flexibility JavaScript often provides, but if you don’t need that, and doing it in CSS is easier, then why not save yourself the trouble?

And the performance of CSS is so much better! Every JavaScript interaction has to go through an event loop that wastes CPU cycles, eats some battery, and adds that tiny bit of stutter to everything.

Sure, in the grand scale of things it isn’t that bad, APIs like requestAnimationFrame are really good at keeping things smooth. But CSS animations run in the separate compositor thread, and aren’t affected by stutters and blocking in the event loop.

It makes quite a difference on low-end devices, but feels nice even on high-end ones. CSS animations on my 240hz monitor look amazing [9] - JS can look pretty good too, but it has that tiny bit of stutter to it that keeps it from being perfect, especially if you plan on running other heavy code at the same time.

It also means you won’t have to worry as much about optimization, as the browser takes care of a lot more of the rendering side of things, and often runs your stuff on the GPU if possible.

Pro tip! Wanna trigger animations from JS anyways? Use the modern Web Animations API to easily play the smooth CSS animations from JS.

Transitioning

Speaking of which, I think it’s time I start showing you practical examples, and a good place to start showing the styles is well, @starting-style.

In the past it has been pretty annoying to add start animations (such as fade-ins) to elements. You’ve had to either set up an entire CSS animation with a separate @keyframes block to go with it, or do a transition using JavaScript where you first add an element to the page, then wait a frame, and then add a class to the element.

.toast {
  transition: opacity 1s, translate 1s;
  opacity: 1;
  translate: 0 0;
  @starting-style {
    opacity: 0;
    translate: 0 10px;
  }
}
Success!
replay

But this has all changed thanks to the new @starting-style at-rule!

Pretty much all you have to do is set your properties as usual, add the initial transition states to @starting-style, and add those properties to a transition. It’s pretty simple and it kind of just works without having to trigger the animation in any way.

Lunalover

Another good example of where CSS shines is theming. Many sites need separate light and dark modes, and modern CSS makes dealing with that pretty easy.

:root {
  color-scheme: light dark;
  --text: light-dark(#000, #FFF);
  --bg: light-dark(#EEE, #242936);
}

By setting the color-scheme property to light dark, you are telling the browser to automatically pick the theme according to the user preference, and you can then make use of that by setting color values with the light-dark() function.

Not only does it set your own colors, but also those of the native components, such as the default buttons, form elements, and scrollbars. It kind of just makes stuff work by default, and that’s nice!

:root {
  color-scheme: light dark;
  &:has(#theme-light:checked) {
    color-scheme: light;
  }
  &:has(#theme-dark:checked) {
    color-scheme: dark;
  }
}

You can then add some way of overriding the color-scheme property to let the user pick a theme different from their system setting. Here I am using radio buttons to accomplish that.

Pro tip! CSS can’t save the theme preference, but you can still do progressive enhancement. Make the themes work CSS-only, and then add the saving/loading of preference as an optional extra in JavaScript or server-side code.

Lyres and accordions

“But those don’t look like radio buttons” I hear you cry.

Input elements such as radio buttons and checkboxes are a great foundation to build other stuff on top of - the example above consists of labels for the buttons and invisible radio buttons that can be checked for with the :checked pseudo-class.

<radio-picker aria-label="Radio buttons example" role="radiogroup">
  <label><input type="radio" name="demo" id="veni" checked>veni</label>
  <label><input type="radio" name="demo" id="vidi">vidi</label>
  <label><input type="radio" name="demo" id="vici">vici</label>
</radio-picker>
<style>
  radio-picker {
    display: flex;
    label {
      &:has(input:checked) {
        box-shadow: inset 0px 0px 8px 0px #888;
      }
      &:has(input:focus-visible) {
        outline: 2px solid #000;
      }
      box-shadow: inset 0px 0px 1.2px 0px #000;
      padding: 10px;
      cursor: pointer;
      background: #0002;
      &:hover { background: #0004; }
      &:active { background: #0006; }
    }
    input {
      /* To allow screen reader to still access these. */
      opacity: 0;
      position: absolute;
      pointer-events: none;
    }
  }
</style>

veni

vidi

vici

This is how I made the theme selector from the previous example. I’ve made the radio buttons half-visible in the demo for clarity, but with the opacity: 0 they would not actually be visible.

There’s a whole lot going on here, so let’s break it down.

<radio-picker aria-label="Radio buttons example" role="radiogroup">

We start off with the radio-picker element - I just made it up, you can use a div instead if you’d prefer. We give it an aria-label to give the group an accessible name, and the aria role of radiogroup to make it work as a group for the radio buttons.

You could also use the fieldset element instead of doing the aria roles if that’d fit your use case better.

<label><input type="radio" name="demo" id="veni" checked>veni</label>
<label><input type="radio" name="demo" id="vidi">vidi</label>
<label><input type="radio" name="demo" id="vici">vici</label>

Next, we add the radio buttons with their respective labels - usually you’d have to use the for attribute on labels to define which element they’re referring to, but since we have the input inside the label we don’t have to do that.

All the type="radio" inputs should also have a name value set to the same thing so that they are grouped together (you still need10 the radiogroup though). And then you can give them values or ids however you want.

label {
  &:has(input:checked) {
    box-shadow: inset 0px 0px 8px 0px #888;
  }
  &:has(input:focus-visible) {
    outline: 2px solid #000;
  }
  box-shadow: inset 0px 0px 1.2px 0px #000;
  padding: 10px;
  cursor: pointer;
  background: #0002;
  &:hover { background: #0004; }
  &:active { background: #0006; }
}

We then style the labels as we wish - the :hover and :active pseudo-classes can be used to make the buttons more fun to click, the :has(input:checked) selector can be used to define the style of the selected button, and the :has(input:focus-visible) selector can be used to add an outline when someone tabs over to the button.

The difference between :focus and :focus-visible is that the former shows up even if you use your mouse, while the latter only shows up when you use keyboard navigation, so it’s often visually more clean to use the latter.

input {
  opacity: 0;
  position: absolute;
  pointer-events: none;
}

And last, we make the radio button input exist while not being visible. This is a bit hacky, but it’s how you can keep this control accessible to keyboard navigation and screen readers.

And that’s how we get the cool-looking radio buttons!

<radio-tabs>
  <div tabindex=0 id="tab-veni">veni...</div>
  <div tabindex=0 id="tab-vidi">vidi...</div>
  <div tabindex=0 id="tab-vici">vici...</div>
</radio-tabs>
<style>
  body:has(#veni:not(:checked)) #tab-veni,
  body:has(#vidi:not(:checked)) #tab-vidi,
  body:has(#vici:not(:checked)) #tab-vici {
    display: none;
  }
</style>

We can now use them in the CSS however we want by just seeing if they’re :checked. Here I made tabs with separate divs for the content by using a :has selector on a parent element to find out which radio button is currently selected.

The :has selector has to be on a parent element that contains both the radio button and the target element - you can simply use html or body if you want it to work across the entire page. You should never use something like :has(…) by itself as it’ll run the selector for every element of the page, which can cause performance issues (body:has(…) is okay).

<div>
  <details name="deets">
    <summary>What's your name?</summary>
    My name is Lyra Rebane.
  </details>
  <details name="deets">
    ...
  </details>
</div>
<style>
  div {
    border: 1px solid #AAA;
    border-radius: 8px;
    /* based on the MDN example */
    summary {
      font-weight: bold;
      margin: -0.5em -0.5em 0;
      padding: 0.5em;
      cursor: pointer;
    }
    details {
      &:last-child { border: none }
      border-bottom: 1px solid #aaa;
      padding: 0.5em 0.5em 0;
      &[open] {
        padding: 0.5em;
        summary {
          border-bottom: 1px solid #aaa;
          margin-bottom: 0.5em;
        }
      }
    }
  }
</style>

Finally, before we move on, I want to give you a quick introduction to the details element. It’s great for if you want an accordion-style menu, such as for a FAQ section. The details open and close independently of each other, but you can set their name attribute to the same value to have only one open at a time.

Using them is pretty easy, put your content and a summary tag inside a details tag, and put the title inside the summary tag. The example above is a bit more convoluted for the visual flair, but all you really need is the html part of it.

The details elements are pretty stylable! You can add animations depending on the [open] state, and you can also get rid of the arrow by setting list-style: none on the summary.

Also, ctrl+f works with it, which is a big win in my book!

Validation

And lastly, I want to show you the power of input validation in HTML and CSS.

<label for="usrname">Username</label>
<input type="text" id="usrname" pattern="\w{3,16}" required>
<small>3-16 letters, only alphanum and _.</small>
<style>
 input:valid {
   border: 1px solid green;
 }
 input:invalid {
   border: 1px solid red;
 }
</style>

This is a simple example of how you can validate an input field with a regex pattern. If you set a pattern attribute like above, a form that contains the input cannot be submitted unless the field matches the pattern. If you’re submitting something like an e-mail address, a phone number, or a url, it might make sense to use the respective input types instead of writing your own regex.

Now, where CSS comes in is styling the input to show whether its value is valid. In the example above, I’m using :valid and :invalid to set a border color, but that comes with the downside of always having your input marked, even if the user hasn’t entered anything yet.

input {
  border: none;
  border-radius: 2px;
  outline: 1px solid #000;
  &:focus { outline-width: 2px; }
  &:user-valid { outline-color: green; }
  &:user-invalid { outline-color: red; }
}

An easy win here is to instead use :user-valid and :user-invalid - these pseudo-classes only become active once you’ve interacted with input field. I also made this example use an outline instead of a border, which I think looks a lot nicer.

It may sometimes even make sense to use a combination of :valid and :user-invalid.

And of course, you can use the :has selector to style other elements depending on the input too!

Password

The password must:

- be 8-16 characters

- contain at least ⅰ roman numeral

- not end with a letter

This one's just for fun ^_-!

I do want to mention that for some stuff, such as date pickers

(
dd/mm/yyyy
)

or datalists

(
pony
), 

there are built-in elements that do the job, but you may find them limited in one way or the other. If you’re making an input like that with specific requirements, you may still need to dip your feet in a bit of JavaScript.

Do not the vw/vh

This section is kind of random but I wanted to include it here because I think a lot of people are messing this one up and I want more people to know how to do this stuff right.

So CSS has vw/vh units that correspond to 1% of the viewport width and height respectively, which makes perfect sense for desktop browsers.

Where it becomes a bit more nuanced is on mobile devices. For example, mobile versions of both Firefox and Chrome will hide the URL bar when scrolling down on a page.

This causes the vw/vh units to be a bit ambigous - do they represent the entire available screen, only the area that’s visible with the URL bar, or something in between?

If it’s the first option, you might end up with buttons or links off-screen [11]! If it’s the second, you may end up with a background div that doesn’t cover the entire background.

[See Original Blog Post]

Above is a table of values your browser reports - if you're on mobile, try scrolling the blogpost up and down so that the URL bar hides and see how the numbers change.

The solution to this is to use the new responsive viewport units: lvh, svh, and dvh.

lvh stands for largest viewport height, and thus is useful for things like backgrounds that you’d want to cover the entire screen with, and wouldn’t care about getting cut off.

svh stands for smallest viewport height, and should be used for things that must always fit on the screen, such as buttons and links.

And dvh stands for dynamic viewport height - this one will update to whatever the current viewport height is. It might seem like the obvious choice, but it should not be used for elements you don’t want resizing or moving around as the user scrolls the page, as it could become quite annoying and possibly even laggy otherwise.

Of course, the respective lvw, svw, and dvw units exist too :).

Keyboard cat

By default, the viewport units do not account for the keyboard overlaying the page.

There are two ways to deal with that: the interactive-widget attribute, and the VirtualKeyboard API.

The former option is widely supported across browsers, works without JS, and goes in the meta viewport tag. It makes it so that opening the keyboard will change all of the viewport units.

<meta name="viewport" content="width=device-width, interactive-widget=resizes-content">

The latter option is currently only supported in Chromium-based browsers, and requires a single line of JavaScript to use:

navigator.virtualKeyboard.overlaysContent = true;

The advantage of the second option is that it allows you to use environment variables in CSS to get the position and size of the keyboard, which is pretty cool.

floating-button {
  margin-bottom: env(keyboard-inset-height, 0px);
}

But considering the fact that it doesn’t work cross-browser, I’d avoid it.

CSS wishlist

Alright, so this is a little different from the rest of the post, but I wanted to bring up some things that I wish were in CSS. I haven’t fully fleshed out all of them, so some definitely wouldn’t fit the spec as-is, but maybe they can inspire some other stuff at least.

They are just fun ideas, don’t take them too seriously.

Reusable blocks

I wish it was possible to put classes in other classes in CSS, so that you could write something like:

.border {
  border: 2px solid;
  border-radius: 4px;
}
.button {
  @apply border;
}
.card {
  @apply border;
}

This is something that Tailwind already has, and that makes me jealous.

Combined @media selectors

We can currently do nested @media queries, and also multiple selectors at the same time:

div {
  &.foo, &.bar {
    color: red;
    padding: 8px;
    font-size: 2em;
  }
  @media (width < 480px) {
    color: red;
    padding: 8px;
    font-size: 2em;
  }
}

But we cannot combine the two into a single selector:

div {
  @media (width < 480px), &.foo {
    color: red;
    padding: 8px;
    font-size: 2em;
  }
}

Which means if you want to do that you’ll inevitably have to repeat code or do some silly variable hacks, neither of which is ideal.

n-th child variable

For many of the CSS crimes I like to commit, I often end up writing code like:

div {
  span:nth-child(1) { --nth: 1; }
  span:nth-child(2) { --nth: 2; }
  span:nth-child(3) { --nth: 3; }
  span:nth-child(4) { --nth: 4; }
  span:nth-child(5) { --nth: 5; }
  ...
  span {
    top: calc(--nth * 24px);
    color: hsl(calc(var(--nth) * 90deg) 100 90);
  }
}

And I think it would be a lot nicer if we could instead just do:

div {
  span {
    --nth: nth-child();
    top: calc(--nth * 24px);
    color: hsl(calc(var(--nth) * 90deg) 100 90);
  }
}

n-th letter targeting

CSS has the ability to style the ::first-letter of text. It’d be cool if were was also a ::nth-letter(…) selector, similar to :nth-child. I suspect the reason this isn’t a thing is because the ::first-letter selector is a pseudo-element, which would be a bit tricky to implement with the nth-letter idea.

/* not a real feature */
p::nth-letter(2) {
  color: red;
}

Blackle suggested that combining the nth-child() variable with :nth-letter targeting would also be fun for certain effects, such as putting the value in the sin() function to create wavy text.

div {
  /* not a real feature */
  --nth: nth-child(nth-letter);
  will-change: transform;
  translate: 0 calc(sin(var(--nth) * 0.35 - var(--wave) * 3) * 5px);
  color: color-mix(in oklch, #58C8F2, #EDA4B2 calc(sin(var(--nth) * 0.5 - var(--wave)) * 50% + 50%));
}

Unit removal

I wish you could easily remove units from values, for example by dividing them.

div {
  /* Turns into:  (no unit) */
  --screen-width: calc(100vw / 1px);
  color: hsl(var(--screen-width) 100, 50);
}

This would allow you to use the size of the viewport or container as a numeric variable for things other than length. For example, the color picker from earlier uses it to convert the location of the color picker dot to a number to be used in a color value instead.

Uh, but wait? Does that mean this feature already exists?

Yeah, lol! We already have the ability to get unitless values in CSS, but it involves doing hacky stuff such as tan(atan2(var(--vw), 1px)) with a custom @property. It’d be nice to have this as just a division, for example.

Oh, and good news, this one we might actually be getting soon!

Also if you do something like calc(1px + sqrt(1px * 1px)) your browser will crash [12].

A better image function

The image() function exists, but no browsers implement it. It’s similar to just using url(), but adds some really cool features such as a fallback color, and image fragments to crop a smaller section out of a bigger image (think spritesheets).

We can already do both fallbacks and spritesheets with the various background properties, but it’d be nice to have this pretty syntax. I’d honestly love this syntax even more for <img> tags than CSS.

style tags in body

I make heavy use of <style> tags in <body> for my projects. On my blog, for example, I write the relevant CSS close to their graphics so that you can start reading the blog before the entire page (or the entire CSS) has finished loading [13]. And it works great!

But what’s unfortunate is that despite browsers supporting this, and major sites using this, it’s not officially spec-compliant. I suspect it’s in the spec to avoid the FOUC footgun, but there are so many reasons you would want/need style in body that I don’t think it justifies it.

I think an HTML validator should warn for this, but not error.

The art

I want to end this article by saying that to me, web development is an art, and thus, CSS is too. I often have a hard time relating to people who do webdev solely to earn money or build a startup - web development is very different when you’re on a team and are given tasks from above instead of having free will over what you create for fun.

It’s probably most apparent with things like AI [14], that for me take all the fun and creativity out of my work. But it also applies to build chain tooling such as linters and minifiers - the way I write my code is part of the art, and I don’t want a tool to erase that. I don’t even use an IDE15.

Among the practical reasons for sticking to CSS listed throughout this post, there’s a secret extra reason I like to do everything in CSS, and that’s expression and art. Art isn’t always practical, and using CSS isn’t either. But it’s how I like to express myself, and it’s why I do what I do.

I tried to keep this post approachable and practical for all web developers. But there is so much more to CSS that I’d like to talk about, so expect another post about the stuff that isn’t practical, and is instead just cool as fuck. I think CSS is a programming language, and I made a game to prove it.

But that’s a topic for another time.

afterword

it’s been almost a year since my last post, but i hope it’s been worth the wait ^_^

As usual, this post is a self-contained HTML file with no JavaScript, images, or other external resources - everything on the page is handwritten HTML/CSS, weighing in at around 49kB gzipped. it was really fun creating all the little interactive widgets and visuals this time around, i think i’ve improved in css a lot since the last time i posted.

This entire post turned out to be a bit of a fun mess (as did i!), it’s almost like a chaotic gradient of tone throughout, i hope it was still interesting and enjoyable to read though.

I have a few new posts in the works: in addition to the second CSS one mentioned earlier, I also have one about a new web vulnerability subclass I discovered, and one about a trans topic. i’m not sure when these posts will come out, but we’ll see! make sure to add me to your rss reader if that sounds fun.

I’ll also be giving a talk at BSides Tallinn in September! i’m hoping to also do css-related talks at the next ccc and disobey, but we’ll have to see whether i get accepted and have the travel budget for those.

Thank you so much for reading <3

If you’d like to reach out, feel free to message me on my socials or at lyra.horse [at] gmail.com.

Discuss this post on: twitter, mastodon, lobsters

  1. Chrome’s DevTools come with the cool flexbox widget. Firefox’s however don’t seem to for some reason? I find that weird because Firefox does have really good tools for flexbox and grid development, so this seems like an odd omission. ↩︎
  2. While I think what I said is true, Tailwind does have more to its existence, the core of which can be found in this post by its creator. ↩︎
  3. You are allowed to just make up elements as long as their names contain a hyphen. Apart from the 8 existing tags listed at the link, no HTML tags contain a hyphen and none ever will. The spec even has <math-α> and <emotion-😍> as examples of allowed names. You are allowed to make up attributes on an autonomous custom element, but for other elements (built-in or extended) you should only make up data-* attributes. I make heavy use of this on my blog to make writing HTML and CSS nicer and avoid meaningless div-soup. ↩︎
  4. Still not nice to read for you? I’m personally not a fan of BEM, but I’d definitely recommend reading up on it too if you just don’t vibe with the way I’m writing my examples. Also, my example intentionally shows off a lot of the syntax at once, but in the real world it might make sense to structure things a little differently. ↩︎
  5. Baseline browsers are Safari (macOS/iOS), Chrome (desktop/Android), Edge (desktop), and Firefox (desktop/Android). ↩︎
  6. The MDN docs of course also list detailed browser compatibility, but the Baseline symbols are nice for just getting a quick “yeah, we can use it and it’ll work for everyone” type overview. ↩︎
  7. ES3 (1999) is the last “classic” version of JavaScript. In 2009 we got the first major revision known as ES5, and a few years later we kicked off the yearly spec updates with ES2015. Also ES4 was abandoned which makes me feel sad :c. ↩︎
  8. 93 files!! Seems like they’re 1/3 functionality, 1/3 ads, and 1/3 analytics. The site works just fine with JavaScript disabled - only stuff like the comments section and ads won’t load. It’s no longer a laggy mess either for some reason. ↩︎
  9. I think the x3ctf challenges page looks really smooth on my computer - the marquee text animation and clicking on the challenges is buttery. And it also runs pretty well on the low-end hardware I have. Note that some browser performance recording tools can act a bit weird with CSS animations, so make sure your tools are working as expected before using them. Unrelated, but I made some other cool x3ctf web stuff too - check out the archive. ↩︎
  10. There’s a bug in Chrome that requires you to use a fieldset/radiogroup for the radio button index to work correctly in screenreaders. Eg if you have 3 radio buttons with the same name, selecting one of them should read “radio button 1 of 3”, which is what Firefox does, but in Chrome it will instead read it as “radio button 4 of 9” or whatever if you don’t have a fieldset/radiogroup because it kind of just combines all the radio buttons on the page into a single index. ↩︎
  11. A certain HR platform I have to use puts its action buttons at the very bottom of a 100vh container, leading to them not being visible/interactable on my phone - not a headache you want to go through when requesting sick days. It’s a good example of how just using the wrong unit can cause a pretty bad real world accessibility problem. ↩︎
  12. Well, probably not. This is a bug I found while writing this post that only affects Chrome, and it’ll probably get fixed before it even manages to hit stable. Update: I took so long to get this blog post out that it has been fixed now. During the writing of this blog post I found another bug in Chrome though, which is pretty funny. Update 2: I found yet another Chrome bug while writing this post, this one is kinda weird, you should read it. ↩︎
  13. This matters for people on slow connections, such as bad mobile data, satellite internet, tor, or iodine. While my blog posts are very small in size, the CSS alone can take up more than the first 14kB of a TCP round trip, so with blocking CSS in the head you might have to wait a few extra seconds (or minutes, in the case of iodine) just to start reading the first paragraph. Now, that 14kB number isn’t completely accurate in the modern world, but testing on my own server (HTTP/2, TLS 1.3), around ~16kB of the compressed html reaches the browser in the first batch of http data. ↩︎
  14. By this I mean tools such as Copilot, Cursor, chatbots etc. I understand there is a huge difference between full-on vibe coding and just using the tab key, but I do not want to use or interact with any of those tools. Please respect that. ↩︎
  15. I write all my code (and blogposts) in Sublime Text, which to me is just a glorified version of Notepad. The features over Notepad it gives me are syntax highlighting, multiple cursors, keyboard shortcuts, and a better visual design. It doesn’t do that much, and yet, it’s perfect. It’s so good I paid for it. ↩︎

It's 2025. All meows reserved.