<div class="wrapper">
  <h1>I am the H1</h1>
  <p>
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Obcaecati, dolorem
    animi iure neque nesciunt quisquam adipisci harum! Mollitia numquam
    architecto veritatis, tenetur molestiae maiores asperiores libero hic veniam
    dolores adipisci?
  </p>
  <ul>
    <li>I am a list item</li>
    <li>I am a list item</li>
    <li>
      I am a list item
      <ul>
        <li>I am a nested list item</li>
        <li>I am a nested list item</li>
      </ul>
    </li>
    <li>I am a list item</li>
  </ul>

  <h2>H2. Big gap above / small gap below</h2>
  <h3>H3. Small gap above / small gap below</h3>
  <p>
    Lorem ipsum dolor sit, amet consectetur adipisicing elit. Magnam cum
    corporis voluptatum quisquam commodi fugit vero, est amet, dolores delectus
    fuga natus porro quo nihil illo ad modi minima saepe.
  </p>
  <p>
    Lorem ipsum dolor sit, amet consectetur adipisicing elit. Magnam cum
    corporis voluptatum quisquam commodi fugit vero, est amet, dolores delectus
    fuga natus porro quo nihil illo ad modi minima saepe.
  </p>
  <h4>H4. Big gap above / small gap below</h4>
  <h5>H5. Small gap above / small gap below</h5>
  <p>
    Lorem ipsum dolor sit amet consectetur, adipisicing elit. Minus doloremque,
    velit aliquid temporibus sapiente, nisi distinctio sequi quod ipsum
    reprehenderit delectus minima exercitationem, amet ut dolorem voluptatum ea
    ratione accusantium!
  </p>
  <h5>H5. Big gap above / small gap below</h5>
  <p>
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione quia at qui
    corrupti blanditiis aspernatur beatae ex, accusamus saepe? Officiis non vel
    omnis dolorem itaque praesentium excepturi, iure sunt est!
  </p>
  <h2>H2. Small gap above / small gap below</h2>
  <p>
    Lorem ipsum dolor sit amet consectetur, adipisicing elit. Illo, facere.
    Dolorem ab debitis neque quam. Maiores, culpa delectus dignissimos accusamus
    quos nihil! Ullam cum pariatur consectetur reprehenderit tenetur libero
    deserunt.
  </p>
  <p>No gap below because I am the last longform text element.</p>
</div>
:root {
  --space-default: 1rem;

  --space-l1-lg: 4rem;
  --space-l2-lg: 3rem;
  --space-l3-lg: 2.5rem;
  --space-l4-lg: 2rem;
  --space-l5-lg: 1.5rem;
  --space-l6-lg: 1rem;

  --space-l1-sm: calc(var(--space-l1-lg) / 4);
  --space-l2-sm: calc(var(--space-l2-lg) / 4);
  --space-l3-sm: calc(var(--space-l3-lg) / 4);
  --space-l4-sm: calc(var(--space-l4-lg) / 4);
  --space-l5-sm: calc(var(--space-l5-lg) / 4);
  --space-l6-sm: calc(var(--space-l6-lg) / 4);

  --space-list-items: 0.5rem; // just for list items
}

// Bare-bones reset
* {
  margin: 0;
}

// By default, every prose element has a bottom margin.
// `li` is excluded because list item spacing is handled later on.
// Same with headings because they have their own, more specific spacing values.
// Feel free to add other elements here e.g. blockquote.
:is(p, ul, ol) {
  margin-bottom: var(--space-default);
}
// Headings have a little more bottom spacing than other prose elements
h1 {
  margin-bottom: var(--space-l1-sm);
}
h2 {
  margin-bottom: var(--space-l2-sm);
}
h3 {
  margin-bottom: var(--space-l3-sm);
}
h4 {
  margin-bottom: var(--space-l4-sm);
}
h5 {
  margin-bottom: var(--space-l5-sm);
}
h6 {
  margin-bottom: var(--space-l6-sm);
}

// Non-heading elements should have a big gap after them if followed by a heading.
// Star selector means that a bunch of stuff should "just work",
// e.g. if you insert images or other elements between text nodes.
// But you could change the `*` to `:is(h1,h2,h3,h4,h5,h6)` if you don't want that.
*:has(+ h2) {
  margin-bottom: var(--space-l2-lg);
}
*:has(+ h3) {
  margin-bottom: var(--space-l3-lg);
}
*:has(+ h4) {
  margin-bottom: var(--space-l4-lg);
}
*:has(+ h5) {
  margin-bottom: var(--space-l5-lg);
}
*:has(+ h6) {
  margin-bottom: var(--space-l6-lg);
}

// Headings followed immediately by a heading of a level down
// should have only a small gap below.
// Using :where() for consistent specificity.
:where(h1):has(+ h2) {
  margin-bottom: var(--space-l1-sm);
}
:where(h2):has(+ h3) {
  margin-bottom: var(--space-l2-sm);
}
:where(h3):has(+ h4) {
  margin-bottom: var(--space-l3-sm);
}
:where(h4):has(+ h5) {
  margin-bottom: var(--space-l4-sm);
}
:where(h5):has(+ h6) {
  margin-bottom: var(--space-l5-sm);
}

// List item spacing.
:where(li):has(+ li) {
  margin-bottom: var(--space-list-items);
}

// Nested list spacing.
// This is the only place we need to use margin-top :'(
:where(li) :is(ul, ol) {
  margin-top: var(--space-list-items);
}

// Remove the bottom margin on the last prose element
// (i.e. if it has no next sibling).
// Could there be an argument to apply this to ALL elements (*) with no next sibling?
:is(p, ul, ol, h1, h2, h3, h4, h5, h6):not(:has(+ *)) {
  margin-bottom: 0;
}

// END

// Unrelated stuff
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  line-height: calc(0.75rem + 1em);
  padding: 40px;
}

.wrapper {
  max-width: 75ch;
  margin: 0 auto;
  border-block: 2px solid red; // to test vertical space around text
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.