Home
/ blog

All the ways to css

I recently finished a little project that was a css birthday card of sorts for a friend. It was bunches of fun doing but recently I looked back on the code and work and had to come to terms with something....my code was bad...really bad.

Why was it bad you ask? I wanted to do something kinda basic where I just stacked divs with different colors in a 'minimalist' way like legos. This is totally ripped from something similar I read a while ago about. I figured it wouldn't be to hard. And well, it wasn't too hard.

I did exactly what I described but with a bit more of position absolute in there for moving completed blocks. Fun stuff. I also used some svg elements in the background to spruce things up a bit.

After looking over the finished result I was and still am quite happy. You can check it out here and also wrote a bit about the process in detail here. Now it wasn't that the code was bad but more so it could be...smaller...simpler...nicer. In theme with my friend who loves making my and others code smaller I wanted to take another stab at specifically a nice way to create minimalist styled "art".

And off we go!

101 ways to css a cat

We want a simple and elegant way of creating stacked colored blocks of differing heights. Note that we do NOT need them to have differing widths. We want it to feel like just stacking blocks.

Something like width 20px, red 100px, green 20px, blue 8px etc. This would result in a 20px wide div with 3 boxes of color with the respective heights 100px, 20px, and 8px. Not so bad.

After a bit of playing around there are...more than a few ways of doing this. As I am sure we all know, css has 101 ways of doing the same thing each with their own tradeoffs. Sure some are obviously not quite as nice as others but still they work.

One thing I wanted to stick with is a single div for everything. Adding more divs gives more options but also can make things more complicated than needed. My first go around used a new div for each block.

How I css a cat

Now after extensive testing (ok really only like 5 minutes) I found that box-shadow was the magic we needed. You see, we can chain many of them together easily and layer them together so it feels like they 'stack'. How is this done?

This

box-shadow: 0 1rem 0 #666, 0 2rem 0 teal, 0 3rem 0 orange;

gives us

The gotchas of how I css cats

Yup...you see that? No, it is left like that for a reason. You should see that the shadow is breaking outside of the div we have because well...that is how box shadow is suppose to work. It will take the size of the element (our div) and apply the box shadow with the options of moving the shadow up up/down and left/right. What we want is it to not move anywhere and just show the shadow inside the element.

Is there anything we can do? 😭 Is this just the end? 😭

Well this is css! Naturally there is a million and one things we can do. We could just add maybe a margin bottom with 100% height. Maybe we could try translating the div up. Position absolute? Sure! that could work.

What will I do? Use the magic of :before and :after.

Now the reason is that again we want a single div and we want it to not break any flows. What does this look like?

.mySelector {
  position: relative;
  width: 4rem;
  height: 3rem;
}
.mySelector:before {
  width: 100%;
  height: 100%;
  box-shadow: 0 0.5rem 0 red, 0 1rem 0 orange, 0 1.5rem 0 yellow, 0 2rem 0 green,
    0 2.5rem 0 blue, 0 3rem 0 purple;
  content: "";
  display: block;
  transform: translateY(-100%);
}

Nice! Now we are stacking down since the default y offset we are giving moves everything down. If we wanted to instead stack up, we'd simply invert the y offset and the translation.

...
box-shadow: 0 -1rem 0 #666, 0 -2rem 0 teal, 0 -3rem 0 orange;
transform: translateY(100%);

Or if we want to be all fancy 🧐 we could use a css variable to control direction.

--stackDirectionScale: -1px;
box-shadow: 0 calc(1rem * var(--stackDirectionScale)) 0 #666, 0 calc(
      1rem * var(--stackDirectionScale)
    ) 0 teal, 0 calc(1rem * var(--stackDirectionScale)) 0 orange;
transform: translateY(calc(100% * var(--stackDirectionScale)));

Very nice but uhh....hard to read. Lets uhh...not do that. We don't need that much flexibility. For now we will just make sure everything lines up manually. Which leads me to another gotcha I found.

More gotchas!

Moar?!?! Really?!? Yes. See, a box shadow can only be as tall or long as the element it is on. Which means that our div needs to be at least as tall as the tallest box shadow we have. This is actually not too bad. All we do is just set the height equal to the sum of all our blocks.

I played around with using ccs variables to have a single height that would scale any number of blocks based on a percentage they took up but it was way WAY!!! too complicated.

☚ī¸ One more gotcha... 😭

...really? More? The last little bit is that if you have a hover style on the before or after, it will apply outside of the containing div like so.

Try hovering or tapping on mobile and you should see that the pseudo element is also triggering the hover event which is translated outside the div. This would also due the same for click events...đŸ˜Ŧ There is a very simple fix though. Just add overflow: hidden to our div.

This works because it will hide the overflow from the pseudo element which prevents any events from firing. Nice but another option is to simply use pointer-events: none; on the pseudo element.

Ok, what is the point?

The point? Now, we can easily start creating all kinds of cool stuff. You like The Simpsons?

What about south park?

This is cool because after we apply some base styling

.base {
  box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);
  margin: 1rem 1rem;
  position: relative;
  width: 2rem;
}
.base:before {
  content: "";
  display: block;
  height: 100%;
  transform: translateY(100%);
  width: 100%;
  z-index: 1;
}

We can now play around easily

.bart,
.lisa {
  height: 90px;
}
.bart:before {
  box-shadow: 0px -30px 0 #34699b, 0px -60px 0 #c02826, 0px -90px 0 #f8c22e;
}
.butters:before {
  box-shadow: 0px -8px 0 #114d33, 0px -32px 0 #50bbab, 0px -60px 0 #fcba94, 0 -70px
      0 #eced26;
}

We could even animate or transition with these as it is all box shadows. Let's just throw something on the :hover to play around.

Super cool right? Oh we are just getting started here. There is so much more we can do. Let's write a quick little react component to randomly toggle a class on a div with transitions.

const characterClassList = [
  "homer",
  "marge",
  "maggie",
  "bart",
  "lisa",
  "kenny",
  "eric",
  "butters",
  "stan",
  "kyle",
];

const MinimalCss = () => {
  const [characterClass, setCharacterClass] = useState("homer");
  const toggleRandomClass = useCallback(() => {
    setCharacterClass(
      characterClassList[Math.floor(Math.random() * characterClassList.length)]
    );
  }, []);
  return (
    <div onClick={toggleRandomClass} className={`${characterClass} base`}>
      <style jsx>{`...earlier styles...`}</style>
    </div>
  );
};

Which will give us.

More css :after reflection

Now since we are only using the :before selector we can do more fun with the :after too! Now I am not sure about you but I think our little box shadow div characters need a nice colored reflection. We can do all of this with just our extra :after selector.

Again, there are many ways we can do this. I am going to be lazy and use some transform stuff. Ok, so here is how we will do it.

:before,
:after {
  // normal styling from before
}
:after {
  z-index: -1;
  filter: blur(6px);
  opacity: 0.3;
  transform: scale3d(1.05, -0.8, 1) translateY(105%);
}

As you can see our trick is to just duplicate the same style and throw a filter blur on it. Than we can just do a scale to flip it upside down. I played around with the transform above to make it look a bit more like a reflection. You should also play around and see what settings you like. This is what the above gives us.

Now we can throw this on all of the previous examples with zero issues...ok not zero issues. As you can see the reflection clips outside our div. We can fix this by adding our height as the bottom margin.

For this to even work I also had to remove overflow: hidden as it was hiding our wonderful reflection. Additionally, filter is not a cheap css property. Hmm... maybe we can do better. There is the backdrop-filter we could also use maybe but I have not played around with it much.

More css :after glow

Just remove the flip from the reflection and we get a nice colored glow.

:after {
  z-index: -1;
  filter: blur(12px);
  opacity: 0.8;
  transform: translateY(0%);
}

Now a cool thing about the filter property is you can layer more together like box shadow.

filter: blur(4px) saturate(5) blur(22px);

Put it all together

Now to put this all together we will create a nifty super awesome little React component that will let us pass in colors and heights just like what we wanted at the start.

Let us outline what we want.

  1. A prop to configure the shadows and colors
  2. Maybe a prop for one of the filter effects

Seems simple enough. Now we can write out our imaginary way we'd want to use this. Than we can write the code that will fullfil what we want.

<Minimal
  blocks={[
    [8, "#114d33"],
    [32, "#50bbab"],
    [60, "#fcba94"],
    [70, "#eced26"],
  ]}
  filter="reflection"
/>

Hmm... Go ahead and look at that for a moment. We could have an array of objects but this keeps all that noisy field stuff out. We also could pass in a string that gets parsed. Regardless of how we do it, we are faced with many questions.

  1. Should the height for a block always be in pixels? should it be a string?
  2. How do we know the total hight to set for the div?
    1. Do we calculate it based on input blocks?
    2. What if it isn't just in pixels?
  3. What about transitions? If the data changes, will it transition by default?
  4. How is the height for a block being interpreted?
    1. Will it stack up or down? Can we control this?

All very good questions and more! But for me, I like stupid simple. The blocks are an array of [heightInPixels: number, validCssColor: string] where we will set the total height to that of the sum of pixel heights. Note, that we are going to interpret each height as the height of the visible block and NOT the total. This is a much nicer when playing around as I won't need to manually add the previous blocks heights with the current block height.

So the above example would look like this with this change.

<Minimal
  blocks={[
    [8, "#114d33"],
    [24, "#50bbab"],
    [28, "#fcba94"],
    [10, "#eced26"],
  ]}
  filter="reflection"
/>

This to me feels more natural and is an api I'd rather work with.

We could totally do some fancy chained css calc() to support almost any unit, but that is like at least 30 minutes of work. And judging by how long this post has taken to write, it would end up being 12 days. Yeah I will uhh...leave that as an exercise for the reader. 😅

For filter we will just have reflection and glow with no options. The current transition curve we have been using will be the default if data is change. No option to turn it off because a life without motion is sad. 😄

Over javascripting

So I started writing out the code for this and found...well...it was ugly. It was an awful lot of code to spit out essentially a single line of css. Now I will show you what I had but this is really all a bit overkill.

const FILTER_REFLECT = "reflection";
const FILTER_GLOW = "glow";

export default ({ blocks, filter = "" }) => {
  const [totalHeight, shadowString] = blocks.reduce(
    ([heightSum, cssString], block, i) => [
      heightSum + block[0],
      `${cssString}${i > 0 ? "," : ""} 0px -${heightSum + block[0]}px 0px ${
        block[1]
      }`,
    ],
    [0, ""]
  );

  return (
    <div className={`${filter}`}>
      <style>{`
        box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);
        height: ${totalHeight}px;
        margin: 1rem auto;
        ${filter === FILTER_REFLECT ? `margin-bottom: ${totalHeight}px` : ""};
        position: relative;
        width: 40px;
        :before,
        .${FILTER_GLOW}:after, .${FILTER_REFLECT}:after {
          transition: all cubic-bezier(0.27, -1.33, 0.58, 1.89) 350ms;
          content: "";
          display: block;
          position: relative;
          height: 100%;
          transform: translateY(100%);
          width: 100%;
          z-index: 1;
          box-shadow: ${shadowString};
          pointer-events: none;
        }
        .${FILTER_REFLECT}:after {
          z-index: -1;
          filter: blur(6px) saturate(2);
          opacity: 0.2;
          transform: scale3d(1, -1, 1) translateY(110%);
        }
        .${FILTER_GLOW}:after {
          z-index: -1;
          filter: blur(4px) saturate(5) blur(22px);
          opacity: 0.8;
          transform: translateY(0%);
        }
      `}</style>
    </div>
  );
};

Works well enough.

This is using nextjs jsx styling. It would be nice if we could write a pure React version using inline styles. Sadly, React decided that css is horrible and bad and stuff. So there is no pseudo element support... 😑 No matter.

There is a way we can still do this in pure react without using a css in js library like styled components, emotion, etc. Here it is with just using the style tag.

export const PureMinimal = ({ blocks, filter = "" }) => {
  const [totalHeight, shadowString] = blocks.reduce(
    ([heightSum, cssString], block, i) => [
      heightSum + block[0],
      `${cssString}${i > 0 ? "," : ""} 0px -${heightSum + block[0]}px 0px ${
        block[1]
      }`,
    ],
    [0, ""]
  );

  return (
    <div className={`${filter} minimals`}>
      <style>{` 
        .minimals {
          box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);
          height: ${totalHeight}px;
          margin: 1rem auto;
          ${filter === FILTER_REFLECT ? `margin-bottom: ${totalHeight}px` : ""};
          position: relative;
          width: 40px;
        }
        .minimals:before,
        .minimals:after {
            transition: all cubic-bezier(0.27, -1.33, 0.58, 1.89) 350ms;
            content: '';
            display: block;
            position: relative;
            height: 100%;
            transform: translateY(100%);
            width: 100%;
            z-index: 1;
            box-shadow: ${shadowString};
            pointer-events: none;
          }
          .minimals.${FILTER_REFLECT}:after {
            z-index: -1;
            filter: blur(6px) saturate(2);
            opacity: 0.2;
            transform: scale3d(1, -1, 1) translateY(110%);
          }
          .minimals.${FILTER_GLOW}:after {
            z-index: -1;
            filter: blur(4px) saturate(5) blur(22px);
            opacity: 0.8;
            transform: translateY(0%);
          }
        `}</style>
    </div>
  );
};

This doesn't support auto prefixing, css modules, etc that things like styled components or emotion have. Still we can than play around a bit easier than before.

Let's wrap this up with a bunch of recreations of things I like. Some of these ideas I got from the minimalist lego ad that went around many years ago while others I did my best to make a minimal version.

Here they are! You can tap/click on them to toggle the active filters. The wrapper doesn't line them up correctly with the reflection filter but ehh I think we can live with that for now 😅

We are not worthy!

Make it so...

Beam me up

Mmmmmkayy.....

dun dum...wooOOoo!

but everything changed...when the fire nation attacked

Thanks for reading!