CSS Basics and Selectors
By the end of this lesson
Target elements precisely without over-specific selectors.
CSS decides how the page looks. You write rules, and each rule has two halves: which elements it applies to, and what should change about them.
The second half is a long list to look up as you need it. The first half — choosing what a rule applies to — is where the skill is, and where most maintenance pain comes from later.
.employee-card {
padding: 16px;
border: 1px solid #d8dde3;
border-radius: 8px;
}- .employee-card is the selector. The leading dot means "any element with employee-card in its class attribute".
- Everything between the braces is the declaration block.
- Each line inside is one declaration: a property, a colon, a value, a semicolon. The final semicolon is optional but leave it in, because the next person to add a line will forget to add it themselves.
- This rule applies to every matching element on the page. There is no need to repeat it per card.
The selectors that cover almost everything you will write:
- p, h1, button
- Type selectors. Match every element of that kind. Useful for setting site-wide defaults, too broad for anything specific.
- .employee-card
- Class selector. Matches any element carrying that class. This is your main tool.
- #expense-form
- Id selector. Matches the one element with that id. Ids must be unique on the page, so this cannot be reused.
- input[type="date"]
- Attribute selector. Matches on an attribute's presence or value, which is handy for form controls.
- .card .name
- Descendant combinator. An element with class name anywhere inside an element with class card. Convenient, and easy to overuse.
- .row:hover, a:focus-visible
- Pseudo-classes. Match a state rather than a kind of element. Anything you style on hover needs a focus style too, or keyboard users cannot see where they are.
- .card, .panel
- A comma means "or". One rule, applied to both.
/* Tied to today's markup */
#directory > div.list > ul li div.employee-name span {
font-weight: 600;
}
/* Describes the thing being styled */
.employee-name {
font-weight: 600;
}- Both rules can produce the same result today. Only the second survives the markup changing.
- The first breaks if the ul becomes an ol, if a wrapper is added or removed, or if the name is no longer inside a span. None of those changes look related to the styling, so the breakage is a surprise.
- The first is also unusable anywhere else. The employee name in a search result is not inside #directory, so it will not match, and someone will copy the rule and change the prefix. Now there are two.
- The second states what it styles. It works wherever a name appears, and it is easy to find: search the codebase for employee-name and you have every use.
Practical habits that keep selectors reusable:
- Give the component a class and style that class. One level, no ancestor path
- Name for meaning: .expense-row, .status-approved, .amount-negative
- Use a second class for a variant — .expense-row.is-draft — rather than a new selector chain
- Set broad defaults with type selectors (body, a, button) and everything specific with classes
- If a selector is more than two parts long, ask what it is compensating for
Summary
- A rule is a selector plus declarations; choosing the selector is the part that needs judgement
- Classes are the main tool because they are reusable and easy to override later
- Keep ids for labels, link targets and script hooks rather than styling
- Long descendant chains encode today's markup into your stylesheet and break when it changes
- Name classes for purpose, not appearance, so they stay true when the design changes
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Flatten some selectors
Rewrite these three so each one is a single class, and say what you would change in the HTML to make that work.
1. #expenses table tbody tr td.amount { text-align: right; }
2. .page .content .card h3 span.label { text-transform: uppercase; }
3. div.list ul li:last-child div { border-bottom: none; }
Show solution
Each rewrite is the same move: name the thing, then style the name. The HTML change is adding one class where you previously navigated to the element.
The third one is worth a second look. It depends on being the last child, which is fine if the visual rule really is "the last row has no bottom border". If the real intent is "rows are separated from each other", a border on the top of every row except the first, or a gap, says it more directly and does not break when order changes.
.expense-amount {
text-align: right;
}
.card-label {
text-transform: uppercase;
}
/* Separate rows instead of un-styling the last one */
.expense-row + .expense-row {
border-top: 1px solid #e4e8ec;
}Think about it
Think about it
A teammate argues that deep selectors are better because they document where a component lives in the page. What is the counter-argument?
Show solution
There is a grain of truth: the selector does tell you where the element sits today. But it records that in the wrong place. A stylesheet is not a map of the HTML, and keeping it accurate means updating CSS every time markup moves.
The cost is paid by whoever changes the markup next, and the failure has no obvious connection to the cause. If you want to document structure, do it in the component and its class names — those travel with the markup instead of shadowing it.
Saved in this browser only.