Bootstrap solved a real problem in 2013. Today, CSS itself does most of what Bootstrap was invented for, and doing it natively produces a smaller, faster site that looks like yours instead of like everyone else’s. This guide walks through building a fully responsive layout with modern CSS only — no framework, no build step, no utility classes.
What you actually get from a framework, and what replaces it
Strip Bootstrap down and it offers four things: a grid, a set of breakpoints, prebuilt components, and a normalising reset. Modern CSS replaces the first two outright with Flexbox, Grid and container queries. The reset is about fifteen lines you can write once. Only the components represent real saved work, and they are also the reason every Bootstrap site looks the same.
The cost side is easy to measure: a framework ships a large stylesheet where you use perhaps ten percent of the rules, and you spend time fighting its opinions whenever your design differs from its defaults.
Start with a sane reset
A few lines prevent most cross-browser layout surprises.
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
body {
min-height: 100vh;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select { font: inherit; }
p, h1, h2, h3, h4 { overflow-wrap: break-word; }
The box-sizing line alone removes an entire category of confusion, because it makes padding and borders count inside an element’s declared width rather than adding to it. The max-width on images prevents the most common cause of horizontal scrolling on phones.
The viewport meta tag
One line in the <head>, and without it none of your media queries will apply on a phone:
<meta name="viewport" content="width=device-width, initial-scale=1">
Mobile browsers otherwise pretend to be a 980px-wide desktop and scale the page down, which is why an unprepared site appears as a shrunken desktop layout.
Design tokens with custom properties
Before writing layout, define your values once:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-accent: #2563eb;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 2rem;
--radius: 8px;
--max-width: 1200px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111827;
--color-text: #f3f4f6;
}
}
This is the part frameworks make hardest and native CSS makes easiest. Dark mode becomes six lines because every colour flows from one place.
Fluid typography without media queries
The clamp() function takes a minimum, a preferred value and a maximum, and scales smoothly between them:
h1 { font-size: clamp(1.75rem, 5vw, 3.5rem); }
p { font-size: clamp(1rem, 2.5vw, 1.125rem); }
The heading never drops below 1.75rem on a small phone and never exceeds 3.5rem on a large monitor, and between those points it scales with the viewport. This replaces three or four font-size overrides per element that a breakpoint-based approach would need.
Use rem rather than px for font sizes so the layout respects a user who has increased their default text size for accessibility reasons.
A container that does not need a grid system
.container {
width: min(100% - 2rem, var(--max-width));
margin-inline: auto;
}
Two lines. The min() function picks whichever is smaller: the full width minus a gutter, or your maximum width. On a phone it gives you 1rem of breathing room on each side; on a desktop it centres at 1200px. No .container-fluid, no nested row and column classes.
Responsive columns that need no breakpoints
This is the technique that most often surprises people coming from a framework:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-lg);
}
Read it as an instruction: fit as many columns as you can, each at least 280px wide, and let them share the leftover space equally. On a 1200px screen you get four columns, on a tablet two, on a phone one — and you never wrote a media query. Change 280px and the whole responsive behaviour changes with it.
auto-fill behaves almost identically but leaves empty tracks when there are few items, which matters if you want a single card to stay narrow rather than stretch.
A page layout with Grid
.layout {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-lg);
}
@media (min-width: 768px) {
.layout {
grid-template-columns: 250px 1fr;
}
}
Note the direction: the default is the single-column mobile layout and the media query adds the sidebar. Writing mobile-first this way means the simplest case needs no overrides, and the styles you ship to the most constrained devices are the smallest.
For named regions, grid-template-areas makes the intent visible in the CSS itself, which is particularly useful when the visual order differs between breakpoints.
Flexbox for components
Grid handles two-dimensional page structure. Flexbox handles one-dimensional component alignment, and the two are complementary rather than competing.
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-md);
}
The flex-wrap: wrap is what makes this responsive with no media query at all: when the items no longer fit, they move to a second line instead of overflowing.
A common card pattern that frameworks charge a lot of markup for:
.card {
display: flex;
flex-direction: column;
}
.card__footer { margin-top: auto; }
That margin-top: auto pushes the footer to the bottom regardless of how much text sits above it, so a row of cards has aligned buttons even with uneven content.
Choosing breakpoints
Do not copy device widths from a list of phone models — the list changes yearly and never covered all devices anyway. Widen your browser slowly and add a breakpoint at the width where the design starts to look wrong. In practice you will usually end up with two or three, somewhere near 600px, 900px and 1200px.
@media (min-width: 40em) { }
@media (min-width: 60em) { }
Using em in media queries means the breakpoints shift if the user has a larger base font size, which is generally the behaviour you want.
Container queries for genuinely reusable components
Media queries respond to the viewport, which is the wrong signal for a component that appears both in a wide main column and a narrow sidebar. Container queries respond to the component’s own available space:
.card-wrapper { container-type: inline-size; }
@container (min-width: 400px) {
.card { display: grid; grid-template-columns: 150px 1fr; }
}
The card becomes horizontal whenever its container is wide enough, regardless of screen size. This is something no CSS framework could offer before the feature existed in browsers, and it is the strongest single argument for writing your own CSS today.
Responsive images
Layout is only half of performance. Serving a 3000px photograph to a phone wastes bandwidth even if CSS scales it down.
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(min-width: 60em) 50vw, 100vw"
alt="Team working at a shared desk"
loading="lazy"
width="800" height="533">
The browser picks the best file for the device. The width and height attributes reserve space so the page does not jump as images load, and loading="lazy" defers offscreen images.
Testing properly
Devtools device emulation is useful but incomplete. Check these specifically: rotate to landscape, zoom the browser to 200 percent, navigate with the keyboard alone, and check that no element causes horizontal scroll. That last one is easy to catch:
* { outline: 1px solid red; }
Apply it temporarily and the offending element becomes obvious.
Accessibility is part of responsive design
A layout that adapts to screen size but not to the person using it is only half responsive. Three additions cost very little.
Respect reduced motion. Some users disable animation because it causes nausea or distraction. Honour that preference:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Keep focus visible. Removing the focus outline because it looks untidy makes keyboard navigation impossible. Replace it rather than deleting it, and use :focus-visible so the ring appears for keyboard users without showing on mouse clicks.
:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 2px;
}
Size tap targets properly. A link that is easy to click with a mouse can be genuinely difficult to hit with a thumb. Give interactive elements adequate padding rather than relying on the text size alone, and keep enough space between adjacent links in a navigation list.
None of this requires a framework, and all of it is easier to get right in your own CSS than by overriding someone else’s component styles.
Frequently asked questions
Is Tailwind a better option than writing plain CSS?
It is a different trade, not a framework in the Bootstrap sense. Tailwind gives you a design system and removes naming decisions, at the cost of verbose markup and a build step. If your objection to Bootstrap was the look-alike components, Tailwind addresses that; if it was the tooling overhead, plain CSS addresses it better.
Will these CSS features work in older browsers?
Grid, Flexbox, custom properties and clamp() have broad support across current browsers. Container queries are newer, so check current support data if you must serve a legacy audience, and treat them as a progressive enhancement rather than a foundation.
How do I organise CSS without a framework’s class system?
A naming convention such as BEM works well, or CSS layers with @layer to control specificity deliberately. The important part is picking one and applying it consistently.
Do I still need a CSS reset?
A small one, yes. Browser defaults differ in small ways that produce inconsistent spacing. The fifteen lines above cover the practical cases without the weight of a full normalise stylesheet.
Conclusion
Responsive design without a framework is no longer the hard path. A short reset, custom properties for your tokens, clamp() for type, min() for containers, auto-fit grids for card layouts, Flexbox for components, and two or three well-chosen breakpoints will carry almost any site. You end up with less code to ship, nothing to fight, and a design that is genuinely yours.