DEV Community

Brendon O'Neill
Brendon O'Neill

Posted on

CSS isn't hard

When I’m browsing the web, I notice that CSS gets a lot of hate. Honestly, I get it, because I used to hate CSS too. But that was before I properly understood how it worked. Most of my frustration came from not really knowing what CSS was doing behind the scenes.

A lot of people jump straight into frameworks like Tailwind without learning the basics first. Tailwind is great, but what many people don’t realise is that understanding CSS makes Tailwind much easier to use. The concepts transfer over almost directly.

For example, this Tailwind:

<div class="flex flex-row justify-center w-60 h-60"></div>
Enter fullscreen mode Exit fullscreen mode

is doing the same kind of job as this CSS:

<div class="box"></div>
Enter fullscreen mode Exit fullscreen mode
.box{
 display: flex;
 flex-direction: row;
 justify-content: center;
 width: 15rem;
 height: 15rem;
}
Enter fullscreen mode Exit fullscreen mode

Once you understand the basics of CSS, you’re not just learning one tool. You’re learning the foundations behind how styling works on the web, which also makes tools like Tailwind feel much less confusing.

So in this post, I’m going to walk through the basics of CSS, from selectors and specificity to sizing and resets, so you can build a stronger foundation and feel more confident styling your projects.

Selectors

Before you can style anything in CSS, you need a way to target the element you want to change. That’s what selectors do.

A selector tells CSS, "Apply these styles to this element." Some selectors are broad and affect lots of elements, while others are more specific and only target one particular thing.

There are quite a few types of selectors in CSS, and each one has its own use case. Some are used all the time, while others are more situational. It’s also worth knowing that selectors have different levels of specificity, which decides which style wins when multiple rules target the same element, but we’ll cover that properly later.

Let’s start with the most common ones.

Type selector

The type selector is usually the first selector people learn. It targets elements by their HTML tag name, like p, h1, div, or button.

This means if you write styles for p, every paragraph on the page will receive those styles unless something more specific overrides them.

p{
 color: red;
 font-size: 20px;  
}

h1{
 color: skyblue;
 font-size: 12px;
}
Enter fullscreen mode Exit fullscreen mode

example of the css p and h1 tags

This is useful when you want to style all elements of the same type at once. For example, if you want every paragraph to have the same text colour or every heading to share a base font size, type selectors are a simple place to start.

Attribute selector

Attribute selectors target elements based on the attributes they contain.

This is especially useful for elements like links, inputs, or form fields, because they often have attributes such as href, title, name, or type.

For example, you might want to style only links with a certain URL, or only inputs with a specific name.

a[title]{
    color: orange;
}

a[href="https://www.youtube.com"] {
  color: cyan;
}


input[name="test"]{
    background-color: wheat;
    color: purple;
}
Enter fullscreen mode Exit fullscreen mode

example of attribute selectors

Here’s what each one is doing:

  • a[title] selects any a tag that has a title attribute
  • a[href="https://www.youtube.com"] selects an a tag with that exact href value
  • input[name="test"] selects an input whose name is "test"

These selectors are great when you need a bit more control without adding extra classes.

Class selector

Class selectors are probably the ones you’ll use the most in day-to-day CSS.

A class selector targets any element with a matching class attribute. This is useful because multiple elements can share the same class, which means you can reuse the same styles wherever you need them.

To write a class selector in CSS, use a dot followed by the class name.

<div class="box"></div>
<p class="heading">I'm a test heading.</p>
Enter fullscreen mode Exit fullscreen mode
.box{
    height: 100px;
    width: 100px;
    background-color: violet;
}

.heading{
    color: tomato;
    font-size: 22px;
}
Enter fullscreen mode Exit fullscreen mode

example of class selectors

This is one of the most common ways to style a page because it gives you flexibility. Instead of styling every p tag the same way, you can choose exactly which paragraph gets a certain style by giving it a class.

ID selector

An ID selector targets an element using its id attribute.

Unlike classes, IDs should be unique. That means one ID should only be used once per page. Because of that, ID selectors are best for elements that are truly one of a kind, like a main heading, a single banner, or a specific section.

To write an ID selector, use a # symbol followed by the ID name.

#rect{
    height: 100px;
    width: 200px;
    background-color:cadetblue;
}

#sub-heading{
    color: tomato;
    font-size: 22px;
}
Enter fullscreen mode Exit fullscreen mode

example of id selectors

ID selectors work fine, but in most modern CSS, classes are usually preferred because they are easier to reuse and manage. IDs also have higher specificity, which can make your styles harder to override later.

Universal selector

The universal selector uses * and matches every element on the page.

That means if you apply a rule using *, it affects everything unless a more specific selector overrides it.

*{
    color: white;
    font-size: 22px;
    font-weight: normal;
}
Enter fullscreen mode Exit fullscreen mode

example of universal selector

This selector can be useful for broad rules, especially in reset styles or when setting something globally like box-sizing.

That said, you usually want to be a little careful with it. Since it applies to everything, it can sometimes affect more elements than you intended.

Nesting selector

CSS nesting lets you write selectors inside other selectors to show the relationship between parent and child elements more clearly.

If you’ve used Sass before, this idea might already look familiar. Native CSS nesting is becoming more common now, and it can make related styles easier to read.

div{
    width: 200px;
    height: 100px;
    background-color: cornsilk;
    padding: 16px;

    & p{
        color: blueviolet;
        font-weight: bold;
    }
}
Enter fullscreen mode Exit fullscreen mode

example of nesting selector

In this example, the nested rule targets any p inside the div.

I don’t personally use nesting all that much, because I usually prefer to name elements clearly and style them directly. But it’s still useful to understand, especially if you come across it in other people’s code or in larger projects.

Now that we know how to target elements, the next step is understanding how to be more precise with those relationships. That’s where combinators come in.

Combinators

Once you understand basic selectors, the next step is learning combinators.

Combinators let us define relationships between elements. Instead of just saying "style all p tags," we can be more specific and say things like "style all p tags inside a div" or "style the first p that comes right after a div".

They’re really useful when your HTML starts getting more structured and you want your CSS to match that structure.

Let’s go through the most common ones.

Descendant combinator

The descendant combinator is written as a space between two selectors.

It selects elements that are inside another element, even if they are nested multiple levels deep.

div p{
        color: blueviolet;
        font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

This means "select every p that is somewhere inside a div".

So if a paragraph is inside the div directly, or even inside another element within that div, it will still be matched.

This is one of the most common combinators because it’s simple and flexible.

Child combinator

The child combinator uses the > symbol.

This one is more specific than the descendant combinator because it only selects elements that are direct children of the parent.

div > p{
        color: blueviolet;
        font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

This means "select any p that is directly inside a div".

If the paragraph is nested deeper, like inside a section that is inside the div, this rule will not apply.

Use this when you only want to target one level down.

Adjacent sibling combinator

The adjacent sibling combinator uses the + symbol.

It selects an element that comes immediately after another element, as long as they share the same parent.

div + p{
        color: blueviolet;
        font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

This means "select the first p that comes directly after a div".

I don’t use this one very often either, but it’s handy when you want to style something based on what comes right before it.

For example, you might want to style the first paragraph after a banner, image, or heading.

General sibling combinator

The general sibling combinator uses the ~ symbol.

This one selects all matching sibling elements that come after the first selector, not just the immediate next one.

div ~ p{
        color: blueviolet;
        font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

This means "select every p that comes after a div, as long as they share the same parent".

So unlike +, the paragraph does not need to come immediately after the div. It just needs to appear later in the same group of siblings.

This can be useful when you want to affect a set of elements that follow another one.

Grouping selectors

This one is not technically a combinator, but it fits naturally here because it’s another useful way to avoid repeating yourself.

If multiple selectors should share the same styles, you can group them together with commas.

div,p,h1{
  color: red;
  background: white;
}
Enter fullscreen mode Exit fullscreen mode

This tells CSS to apply the same styles to all div, p, and h1 elements.

Grouping selectors is great for keeping your CSS shorter and cleaner, especially when several elements need the same rules.

Combinators are all about relationships. They help you target elements based on where they are and how they connect to other elements in your HTML.

Now that we’ve covered selectors and relationships between elements, the next thing to look at is how elements can be styled in different states. That’s where pseudo-classes come in.

Pseudo classes

Pseudo-classes let us style elements based on their state.

Instead of just targeting what an element is, we can target how it is behaving. Common examples are when a user hovers over something, clicks it, or focuses on an input field.

You’ll use these a lot for buttons, links, forms, and interactive elements.

div{
        width: 200px;
        height: 200px;
        background-color: aqua;
}

div:hover{
    background-color: chocolate;
}
Enter fullscreen mode Exit fullscreen mode

example of pseudo classes

In this example, the div starts with an aqua background. When the user hovers over it, the background changes to chocolate.

Some common pseudo-classes you’ll come across are:

  • :hover when the user moves over an element
  • :active when the element is being clicked
  • :focus when an input or button is selected
  • :first-child selects the first child inside a parent

They’re a simple but powerful way to make your page feel more interactive.

Now that we know how to target elements in different states, the next thing to understand is what happens when multiple CSS rules target the same element. That’s where specificity comes in.

Specificity

As your CSS grows, you’ll eventually run into moments where two different rules are trying to style the same element. When that happens, CSS has to decide which rule wins. That decision is based on specificity.

Specificity is basically the weight of a selector. The more specific the selector is, the more likely it is to override another one.

For example:

p {
  color: blue;
}

.text {
  color: green;
}

#main-text {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

If all three selectors target the same paragraph, the #main-text rule wins because an ID selector is more specific than a class selector, and a class selector is more specific than a type selector.

A simple way to think about the order is:

  • type selectors like p or div
  • class selectors, attribute selectors, and pseudo-classes
  • ID selectors

So in general:

  • p is weaker than .text
  • .text is weaker than #main-text

Orders

If two selectors have the same specificity, then the rule written later in the CSS wins.

p {
  color: blue;
}

p {
  color: red;
}
Enter fullscreen mode Exit fullscreen mode

Here, the paragraph will be red because that rule comes later.

This is why the order of your CSS file matters, especially when you reuse selectors.

Specificity is one of those CSS ideas that feels annoying at first, but once you understand it, a lot of styling bugs start to make sense.

Next, let’s look at sizing, because choosing between units like px, rem, and em is another big part of writing CSS well.

Sizes

Sizing in CSS can feel a little confusing at first because there are several different units, and they don’t all behave the same way. Over time, you’ll find the ones that make the most sense for different jobs.

When it comes to fonts, the main units you’ll usually see are px, em, and rem.

  • px is a fixed unit
  • em is relative to the font size of the parent element
  • rem is relative to the root HTML font size
h1 {
  font-size: 32px;
}

p {
  font-size: 1rem;
}

.small {
  font-size: 0.8em;
}
Enter fullscreen mode Exit fullscreen mode

example of font sizes

Out of these, I personally prefer using rem most of the time. It keeps sizing more consistent across a project and is usually easier to manage than em, especially once elements start nesting inside each other.

After font sizing, you’ll also come across units like % and vh when working with containers and layout.

  • % is relative to the size of the parent element
  • vh and vw stand for viewport height and width, so 100vh means the full height of the browser window and 100vw is the full width of the browser window
.container {
  width: 80%;
}

.hero {
  height: 100vh;
}
Enter fullscreen mode Exit fullscreen mode

These are useful for creating flexible layouts. % works well when you want something to scale with its parent, and vh is common for sections like hero banners that should fill the screen.

There are plenty of sizing units in CSS, but these are some of the main ones you’ll use early on. For me, rem is usually the best choice for font sizing, while %, vw and vh are great for building containers and larger layout sections.

Next, let’s look at margin and padding, because choosing the size of an element is only part of the job, spacing it properly matters too.

Margin and padding

Once you’ve sized an element, the next thing to think about is spacing. This is where padding, margin, and borders come in.

example of how margin and padding works

Padding is the space inside an element. It sits between the content and the border.

Margin is the space outside an element. It creates distance between that element and the surrounding elements.

Borders sit between the padding and the margin. They wrap around the content and padding, and they can be styled with different widths, colours, and line types.

.box {
  padding: 20px;
  margin: 20px;
  border: 2px solid black;
}
Enter fullscreen mode Exit fullscreen mode

example of margin and padding

In this example:

  • padding: 20px adds space inside the box
  • margin: 20px adds space outside the box
  • border: 2px solid black adds a visible edge around it

One small CSS trip-up that catches many people is margin collapsing. This usually happens with vertical margins.

If two elements sit on top of each other and both have margins, those margins do not always add up. Instead, they can collapse into one margin, and usually the larger value wins.

.box-one {
  margin-bottom: 20px;
}

.box-two {
  margin-top: 30px;
}
Enter fullscreen mode Exit fullscreen mode

example of margin collapsing

You might expect the total space between these boxes to be 50px, but in many cases it will only be 30px because the margins collapse.

This is one of the reasons I usually prefer using padding for spacing inside sections or containers. Padding feels more predictable, and it’s also often how I create height. Instead of forcing an element to have a fixed height, I’d usually rather let the content define the height naturally and use padding to give it space.

That tends to make layouts more flexible, especially when content changes or grows.

Spacing in CSS gets much easier once you understand where that space is actually being added. And one small reset rule can make this even easier to manage, setting box-sizing properly goes a long way.

Reset script

One of the most useful things you can add at the start of a CSS file is a reset.

Browsers all come with their own default styles. Things like margins, padding, heading sizes, and list spacing are often applied automatically before you write any CSS yourself. That can make your page look inconsistent across elements right from the start.

A reset is simply a way of clearing out some of those browser defaults so you can decide your own starting point.

It doesn’t have to be anything complicated either. A reset can be as small as setting box-sizing, margin, and padding for everything on the page.

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
Enter fullscreen mode Exit fullscreen mode

This is a really common place to start.

  • margin: 0 removes the browser’s default outer spacing
  • padding: 0 removes the default inner spacing
  • box-sizing: border-box makes width and height easier to work with, because padding and borders are included in the final size

Without border-box, if you set an element to width: 200px and then add padding, the element becomes wider than 200px. With border-box, the padding stays inside that width, which makes layouts much easier to control.

The nice thing about resets is that they can grow with you. You can start with a very basic one like this, and then build on it over time as you get more comfortable with CSS and figure out what defaults you do or don’t want.

So a reset isn’t really about deleting styles for the sake of it, it’s about giving yourself a cleaner and more predictable starting point.

Conclusion

CSS can feel frustrating at first, but once you understand the basics, it starts to make a lot more sense.

We’ve covered selectors, combinators, pseudo-classes, specificity, sizing, spacing, and resets, all the core pieces that help you build and style pages with more confidence.

The more you understand plain CSS, the easier it becomes to use any styling tool built on top of it, including Tailwind.

Start simple, keep practising, and over time CSS becomes a lot less of a struggle and a lot more of a tool you can rely on.

Top comments (0)