By the end of today you can
- explain why a CSS declaration wins or loses;
- use cascade layers to control ownership;
- distinguish raw tokens from semantic tokens;
- choose normal flow, Flexbox, Grid, or positioning intentionally;
- use intrinsic sizing,
minmax(),clamp(), andsubgrid; - distinguish viewport responsiveness from container responsiveness;
- build direction-independent layouts with logical properties;
- use modern selectors without creating specificity traps;
- compare plain CSS, CSS Modules, utility CSS, and CSS-in-JS by problem;
- design a responsive multilingual catalogue without JavaScript measurement.
CSS is a system, not decoration
A dashboard must answer architectural questions:
- What happens when the page narrows?
- What happens when a card moves into a narrow sidebar?
- How do cards align when descriptions have different lengths?
- How do long translations affect the layout?
- Which rule wins when a library and the application disagree?
- How can one design decision update the whole interface?
These are system-design questions expressed through CSS.
Browser, user, and author styles
The page starts with more than our stylesheet:
- the browser supplies user-agent styles;
- users may supply preferences or styles;
- the author supplies application styles.
An unstyled <h1> is already bold, large, and separated from nearby text.
Author CSS is not the only styling authority. This matters especially for
accessibility preferences and !important declarations.
Inheritance is an architectural tool
body {
color: #222;
font-family: system-ui, sans-serif;
}Descendant text normally receives these values without repeating them on every element.
Properties such as color and font-family commonly inherit. Properties such
as margin, padding, border, and width normally do not.
Use inheritance for broad design decisions, then override at meaningful boundaries.
Specificity is not a quality ranking
.card h2 { color: navy; }
#dashboard .card h2.title {
color: purple;
}The second selector is harder to override, but that does not make it better.
High specificity increases the cost of future changes.
Prefer a selector that is easy to reason about and easy to replace.
Do not reduce specificity to arithmetic alone
Specificity is one stage in the cascade, not the entire cascade.
Before comparing selectors, ask:
- Are both declarations in the same origin?
- Are they in the same layer?
- Is one declaration important?
- Is a scoped rule involved?
- Only then: which selector is more specific?
The browser is resolving a precedence system, not grading CSS style.
Cascade layers make ownership explicit
@layer reset, base, components, utilities;
@layer reset { /* normalize browser differences */ }
@layer base { /* element defaults and typography */ }
@layer components { /* cards, forms, navigation */ }
@layer utilities { /* small explicit overrides */ }The layer order is declared once. A later layer can win without requiring an increasingly specific selector.
Layer precedence comes before specificity
@layer base {
#app .button { color: blue; }
}
@layer components {
button { color: green; }
}The simple selector in the later components layer can win over the highly
specific selector in the earlier base layer.
This lets architecture control precedence before selector complexity grows.
A practical layer architecture
@layer reset, tokens, base, components, utilities, overrides;Possible ownership:
| Layer | Responsibility |
|---|---|
reset | predictable browser baseline |
tokens | custom properties and theme values |
base | elements, typography, document defaults |
components | reusable UI boundaries |
utilities | small intentional helpers |
overrides | explicit application exceptions |
The names matter less than the stable ownership contract.
Theming with custom properties
:root {
--surface-page: #ffffff;
--text-primary: #172a42;
}
[data-theme="dark"] {
--surface-page: #0c2238;
--text-primary: #f5f7fa;
}
body {
background: var(--surface-page);
color: var(--text-primary);
}The component stays attached to meaning. The theme changes the values.
Positioning changes the relationship
.badge {
position: absolute;
inset-block-start: 0.5rem;
inset-inline-end: 0.5rem;
}Absolute positioning can be correct for a badge anchored to a card. It is a poor replacement for a page layout system when content can grow or translate.
Use fixed and sticky positioning with equally explicit viewport and scrolling assumptions.
Main axis and cross axis
.toolbar {
display: flex;
flex-direction: row;
}For a row:
- the main axis is inline/horizontal in the usual LTR case;
- the cross axis is block/vertical.
Use justify-content for main-axis distribution and align-items for
cross-axis alignment. Always consider writing direction before describing an
axis as “left” or “right”.
Flex sizing is not just alignment
.toolbar__search {
flex: 1 1 20rem;
min-inline-size: 0;
}Flex sizing considers:
- the flex basis;
- grow and shrink factors;
- available free space;
- minimum sizes;
- intrinsic content size.
min-inline-size: 0 can be necessary when a flexible child must be allowed to
shrink rather than force overflow.
Grid tracks express constraints
.catalogue {
grid-template-columns:
repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}This says:
- create as many tracks as fit;
- do not make a card narrower than its useful minimum;
- share remaining space between tracks.
The layout responds to available space without a device list.
Intrinsic sizing asks the content
.label {
inline-size: fit-content;
}Useful intrinsic concepts include:
min-content: the smallest size without avoidable breaking;max-content: the size needed by the unwrapped content;fit-content(): a bounded content-aware size.
Intrinsic sizing is especially important when translations and user content change the amount of text.
Grid or Flexbox?
| Question | Prefer |
|---|---|
| Is the layout mainly one axis? | Flexbox |
| Are rows and columns both meaningful? | Grid |
| Does content define a small toolbar? | Flexbox |
| Does the page define a shared card matrix? | Grid |
| Does one element need to distribute remaining space? | Flexbox |
They are complementary systems, not competing religions.
subgrid shares tracks intentionally
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}subgrid allows descendants to participate in the parent grid’s track sizing.
It is useful when card headings, descriptions, and actions should align across
cards with different content lengths.
Do not use subgrid automatically
Use it when shared track alignment is a real requirement.
Avoid it when:
- cards have independent internal structure;
- the extra coupling makes the component harder to reuse;
- a simpler normal flow is sufficient;
- the alignment is only decorative.
Layout power should follow a demonstrated relationship.
Container queries answer component-space questions
.catalogue-shell {
container: catalogue / inline-size;
}
@container catalogue (width < 36rem) {
.catalogue-toolbar {
flex-direction: column;
}
}The component responds to the space it actually receives, whether that space comes from the page, a sidebar, or a nested layout.
Media queries and container queries differ
| Question | Tool |
|---|---|
| Is the viewport narrow? | Media query |
| Is this component’s parent narrow? | Container query |
| Does the user prefer reduced motion? | Media query |
| Should a card switch from horizontal to vertical? | Container query |
Container queries do not replace media queries. They solve a different scope of responsive reasoning.
Responsive images belong to layout
<img
src="catalogue-small.webp"
srcset="catalogue-small.webp 640w,
catalogue-large.webp 1280w"
sizes="(width < 48rem) 100vw, 50vw"
alt="A service catalogue dashboard">Image dimensions, aspect ratios, loading behaviour, and object fitting affect layout stability and performance together.
CSS has logical axes
Prefer logical properties when the interface can change direction:
.card {
margin-inline: auto;
padding-block: 1rem;
padding-inline: 1.25rem;
border-inline-start: 0.25rem solid var(--color-accent);
}This expresses the relationship instead of hard-coding physical left and right values.
Direction-independent components
.toolbar {
display: flex;
gap: 1rem;
margin-inline-start: auto;
}
.status {
border-inline-start: 0.3rem solid var(--status-color);
padding-inline-start: 0.75rem;
}Test the component with different dir values rather than copying the whole
stylesheet and swapping every physical property.
Modern selectors express relationships
CSS can now describe relationships more directly:
/* Group alternatives without repeating a block */
:is(.primary, .secondary) { border-radius: 0.5rem; }
/* Match without adding specificity */
:where(.card h2) { margin-block-end: 0.5rem; }Use these features to make intent clearer, not to hide an unmaintainable selector strategy.
:has() selects based on a relationship
.field:has(input:invalid) {
border-color: var(--color-error);
}The parent can respond to the state of a descendant without JavaScript merely to add a class.
Still check whether the selector expresses a stable relationship and whether a class would make the ownership clearer.
User preferences are part of responsiveness
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Other preferences can affect colour, contrast, and interaction assumptions. Responsive design includes the user’s environment, not only its dimensions.
Styling architecture is a dependency system
Every approach answers questions about:
- where styles live;
- how names are scoped;
- how styles are composed;
- how themes are represented;
- how overrides work;
- how unused styles are removed;
- how runtime and build-time costs are distributed.
The right choice depends on the project’s constraints and ownership boundaries.
Plain CSS
Strengths:
- native browser model;
- no framework requirement;
- easy to inspect in DevTools;
- supports layers, custom properties, and modern selectors.
Risks:
- unclear naming can create collisions;
- global ownership can become ambiguous;
- unused styles need a deliberate strategy.
Architecture is still required even when no tool generates the CSS.
Component-oriented CSS and CSS Modules
Component-oriented CSS groups styles around an interface boundary.
CSS Modules add build-time scoping:
They reduce accidental collisions, but they do not decide:
- token ownership;
- layout responsibility;
- accessibility;
- responsive strategy;
- whether a component boundary is well designed.
Utility-first CSS
Utility classes make small decisions explicit in markup:
<div class="grid gap-4 md:grid-cols-3"></div>Benefits can include consistency and fast composition. Costs can include noisy markup, difficult component-level meaning, or a false belief that utilities remove architecture.
Utilities are a vocabulary, not a complete design system.
CSS-in-JS is not one technology
CSS-in-JS can mean different things:
- runtime style generation;
- build-time extraction;
- component-local syntax;
- theme-aware value functions;
- atomic style generation.
Compare the actual tool’s runtime cost, debugging model, SSR behaviour, accessibility support, and ownership boundaries - not the category label.
Compare problems, not fashion
Ask:
- How many teams own the styles?
- Do styles need to work without a framework runtime?
- How important is runtime theming?
- What is the build and delivery environment?
- How will developers debug the final CSS?
- How will shared components evolve?
Choose the smallest architecture that meets the real constraints.
Build the page layout
.page {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--space-6);
inline-size: min(100% - 2rem, 80rem);
margin-inline: auto;
}
@media (width > 60rem) {
.page { grid-template-columns: 16rem minmax(0, 1fr); }
}The page breakpoint responds to the viewport. The components inside it should still respond to the space each one receives.
Build the toolbar with Flexbox
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
.toolbar__search {
flex: 1 1 18rem;
min-inline-size: min(100%, 12rem);
}Wrapping is part of the design. It is better than forcing every control into a single row that cannot contain translated labels.
Adapt cards to their container
.product-grid { container: products / inline-size; }
.product-card {
display: grid;
gap: 0.75rem;
}
@container products (width > 42rem) {
.product-card {
grid-template-columns: 1fr auto;
}
}The card does not need to know whether the product grid is on the full page or inside a sidebar.
Make the catalogue RTL-safe
.catalogue {
padding-inline: 1rem;
margin-inline: auto;
}
.status {
border-inline-start: 0.25rem solid var(--status-color);
padding-inline-start: 0.75rem;
}Test English, Arabic, and Sorani Kurdish content with long labels. Do not
assume that replacing left with right is localization.
Misconceptions to leave behind (Part 1)
| Misconception | Better model |
|---|---|
| Specificity decides every conflict. | Origin, layers, specificity, and order all matter. |
| The most specific selector is best. | The most maintainable selector is usually better. |
| Flexbox replaced Grid. | Flexbox and Grid solve different dimensional problems. |
| Grid replaced Flexbox. | Tool choice follows the relationship being laid out. |
| Responsive means phone/tablet/desktop. | Respond to constraints and user environment. |
Misconceptions to leave behind (Part 2)
| Misconception | Better model |
|---|---|
| Container queries replace media queries. | They answer different scope questions. |
| RTL means swap every left and right. | Use logical properties and inspect meaning. |
| CSS variables are text substitution. | They cascade, inherit, and change at runtime. |
| CSS Modules solve architecture. | Scoping helps; ownership and layout still need design. |
| Utility CSS removes architecture. | A utility vocabulary still needs rules and boundaries. |
Troubleshooting questions (Part 1)
| Symptom | First question |
|---|---|
| A rule will not win | Which layer, specificity, origin, and order are involved? |
| A flexible item overflows | Is its minimum size preventing shrinkage? |
| Cards do not align | Is shared track alignment actually required? |
| A component breaks in a sidebar | Is it using a viewport query instead of a container query? |
Completion check
- I can explain why a declaration wins.
- I can use cascade layers to represent ownership.
- I can distinguish raw tokens from semantic tokens.
- I can choose Flexbox, Grid, or normal flow for a reason.
- I can use intrinsic sizing and
minmax()without a device list. - I can explain when
subgridis useful. - I can distinguish media queries from container queries.
- I can use logical properties for direction-independent layout.
- I can respect reduced motion and long translated content.
- I can compare styling architectures by project constraints.
- I completed the responsive multilingual catalogue practical.