How to Make a Beautiful Website with CSS

Take the working-but-ugly form from the last lesson and make it something you would actually show people — using custom properties, flexbox, grid and a real type scale.

PHP & the Web Beginner 16 min read Watch the video (3:59)

Your form works. It also looks like it was built in 1997. Let us fix that — not with a framework, but by understanding the five ideas that make modern CSS good.

The uncomfortable truth about design: it is rarely about picking prettier colours. It is about consistency and space. Most amateur pages use eleven slightly different greys and fourteen arbitrary margins. Most professional pages use six greys and spacing from a fixed scale. That is the whole secret.

#1. Custom properties — define your system once

css
:root {
  /* Colour */
  --brand-50:  #f0f7ff;
  --brand-100: #dbeeff;
  --brand-300: #85c4ff;
  --brand-500: #1b84f3;
  --brand-600: #0a68d4;
  --brand-700: #0a52a8;

  --ink:       #0f1e33;
  --ink-muted: #55657d;
  --surface:   #ffffff;
  --page:      #f5f9ff;
  --line:      #dbe6f5;

  /* Spacing — every gap comes from here, nowhere else */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;

  /* Type scale — a fixed ratio, not arbitrary numbers */
  --text-sm:  0.875rem;
  --text-base: 1rem;
  --text-lg:  1.125rem;
  --text-xl:  1.5rem;
  --text-2xl: 2rem;
  --text-3xl: 2.75rem;

  --radius: 12px;
  --shadow: 0 1px 2px rgb(15 30 51 / 0.06),
            0 8px 24px -8px rgb(15 30 51 / 0.12);
}

Now everything references the system:

css
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-6);
  box-shadow: var(--shadow);
}

Two payoffs. First, changing the brand colour is one edit instead of forty. Second — and this is the real one — when the only available spacing values are --space-1 through --space-12, you physically cannot introduce a rogue 13px margin. The system enforces the consistency for you.

#2. Sane defaults

Every stylesheet should start with these:

css
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--ink);
  background: var(--page);
  -webkit-font-smoothing: antialiased;
}

img, svg, video {
  display: block;
  max-width: 100%;
  height: auto;
}

input, button, textarea, select {
  font: inherit;   /* form controls do NOT inherit fonts by default */
}

box-sizing: border-box means width: 300px stays 300px once you add padding and a border. The default (content-box) adds them on top, which is the source of an enormous amount of "why is my layout 32 pixels too wide" frustration.

font: inherit on form controls is the one people forget. Without it, your carefully chosen font stops at the edge of every <input> and the browser substitutes something from 1995.

line-height: 1.6 on body text is the single highest-impact typography change you can make. Cramped lines are the main reason amateur pages feel hard to read.

#3. Flexbox for one dimension

Flexbox arranges things along a line — a row or a column.

css
.nav {
  display: flex;
  align-items: center;        /* vertical centring, finally solved */
  gap: var(--space-6);        /* space between, with no margin hacks */
}

.nav__spacer {
  margin-left: auto;          /* pushes everything after it to the right */
}

gap is worth pausing on. Before it existed, spacing children meant margin-right on every item plus a :last-child rule to remove the final one. gap puts space between items only. Never fake it with margins again.

margin-left: auto on a flex child absorbs all remaining space, which is the cleanest way to push a logo left and links right.

Centring anything, in three lines:

css
.center {
  display: flex;
  align-items: center;        /* cross axis */
  justify-content: center;    /* main axis */
}

#4. Grid for two dimensions

Grid handles rows and columns together. This one line is the most useful in modern CSS:

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-6);
}

Read it as: as many equal columns as fit, each at least 280px wide. Three columns on a desktop, two on a tablet, one on a phone — with no media queries at all. The layout responds to available space rather than to guessed device sizes.

For a page shell:

css
.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  max-width: 72rem;
  margin-inline: auto;
  padding-inline: var(--space-6);
}

@media (min-width: 900px) {
  .layout--with-sidebar {
    grid-template-columns: 16rem minmax(0, 1fr);
    gap: var(--space-12);
  }
}

#5. Make the form look good

Now the actual payoff. Here is the contact form from the last lesson, restyled:

css
.form {
  display: grid;
  gap: var(--space-5, 1.25rem);
  max-width: 34rem;
  padding: var(--space-8);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.field {
  display: grid;
  gap: var(--space-2);
}

.field label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-muted);
  letter-spacing: 0.01em;
}

.field input,
.field textarea {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  color: var(--ink);
  background: var(--brand-50);
  border: 1.5px solid var(--line);
  border-radius: 10px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}

.field input:hover,
.field textarea:hover {
  border-color: var(--brand-300);
}

.field input:focus,
.field textarea:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--brand-500);
  box-shadow: 0 0 0 4px rgb(27 132 243 / 0.15);
}

.field input[aria-invalid="true"] {
  border-color: #e5484d;
  background: #fff5f5;
}

.field .error {
  font-size: var(--text-sm);
  color: #c62a2f;
  min-height: 1.25em;   /* reserve the space so nothing jumps */
}

.button {
  padding: var(--space-3) var(--space-6);
  font-weight: 650;
  color: white;
  background: linear-gradient(180deg, var(--brand-500), var(--brand-600));
  border: none;
  border-radius: 10px;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
  box-shadow: 0 1px 2px rgb(10 82 168 / 0.3), 0 6px 16px -6px rgb(10 82 168 / 0.5);
}

.button:hover {
  filter: brightness(1.06);
  transform: translateY(-1px);
}

.button:active {
  transform: translateY(0);
}

Four details that separate this from a beginner stylesheet:

Never outline: none on its own. Removing the focus ring without replacing it makes your form unusable for anyone navigating by keyboard. The box-shadow above is the replacement — and it looks better than the default. Removing the indicator entirely is an accessibility bug, not a design choice.

min-height on the error message. Without it, the message appearing pushes everything below it down, and the whole form jumps. Reserving the line means errors fade in without moving anything.

Transition specific properties. transition: all also animates layout properties, which forces the browser to recalculate geometry every frame. Name the properties you actually change.

A 1px lift on hover. translateY(-1px) is barely perceptible consciously, and it makes a button feel physical. Most polish is this small.

#Dark mode is nearly free

Because every colour is a variable, supporting dark mode means redefining seven values:

css
@media (prefers-color-scheme: dark) {
  :root {
    --ink:       #e8f0fb;
    --ink-muted: #94a6bf;
    --surface:   #131c2b;
    --page:      #0b1220;
    --line:      #22304a;
    --brand-50:  #16233a;
    --shadow: 0 1px 2px rgb(0 0 0 / 0.4),
              0 8px 24px -8px rgb(0 0 0 / 0.6);
  }
}

Not one component rule changes. This is the argument for custom properties in a nutshell.

#Respect motion preferences

css
@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;
  }
}

Some people get genuinely motion sick from parallax and large transitions. This block is five lines and it is not optional.

#The polish checklist

Before you call any page finished:

  • All spacing comes from the scale. No stray 7px.
  • Body text has line-height of at least 1.5 and a measure of 60–75 characters (max-width: 65ch).
  • Every interactive element has a visible :focus-visible style.
  • Text contrast is at least 4.5:1 against its background.
  • Hover states exist on everything clickable, and cursor: pointer on buttons.
  • It works at 360px wide. Check it.
  • No horizontal scrollbar at any width.
  • prefers-reduced-motion is handled.

Check yourself

4 questions on what you just read. No score is kept — it is only here to catch the bits that did not stick.

  1. 1What does a fixed spacing scale actually buy you?

    Show the answer

    A. Rogue values become impossible, so spacing stays consistent by construction.

    Most amateur pages use fourteen arbitrary margins. When the only options are --s-1 through --s-24, you cannot introduce a stray 13px.

  2. 2Which line gives you responsive columns with no media queries?

    Show the answer

    A. grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));

    Read it as "as many equal columns as fit, each at least 280px". The layout responds to available space rather than to guessed device sizes.

  3. 3A long URL in one grid column gives the whole page a horizontal scrollbar. Fix?

    Show the answer

    A. Use minmax(0, 1fr) instead of 1fr for the track.

    A bare 1fr track will not shrink below its content's intrinsic width. This is the cause of most mystery horizontal overflow.

  4. 4What is wrong with outline: none on a focused input?

    Show the answer

    A. It removes the only cue a keyboard user has about where they are.

    Removing the ring is fine — as long as you replace it. A box-shadow ring usually looks better than the default anyway.

#Key takeaways

  • Consistency beats decoration. A fixed spacing scale and a small palette do most of the work.
  • Custom properties turn a theme change — including dark mode — into a handful of edits.
  • box-sizing: border-box and font: inherit on form controls belong in every reset.
  • Flexbox for a line, Grid for a plane. repeat(auto-fit, minmax(280px, 1fr)) is responsive with no media queries.
  • Use minmax(0, 1fr) to stop long content blowing out grid columns.
  • Never remove a focus ring without replacing it, and always honour prefers-reduced-motion.

That completes the PHP and web track. You now have a working, validated, good-looking web application built from nothing.

Finished this one?

The challenge above has no posted solution — that is deliberate. Get it working, then come back for the next lesson.

More tutorials Subscribe