Skip to content
Becoming a design system designer

Part 11 of 14 · · 6 min read

How much HTML, CSS and JavaScript a designer needs

Enough code to work with engineers: semantic HTML, Flexbox and Grid, custom properties, container queries, :has(), and the UI states JavaScript reveals.

A design system designer doesn’t need to write production code, but they do need to read it. Knowing the basics of HTML, CSS and JavaScript tells you which designs are cheap to build and which are expensive, and lets you talk with engineers in the same terms.

This part covers the working knowledge most job descriptions mean by “understands implementation constraints”: enough to make better design decisions and better specs, not enough to replace a developer.

Semantic HTML: say what a thing is

HTML elements carry meaning. Screen readers, search engines and the browser itself rely on that meaning, so the right element is an accessibility decision as much as a technical one.

html

<header><nav>…</nav></header>
<main>
  <h1>Databases</h1>
  <section aria-labelledby="pg">
    <h2 id="pg">PostgreSQL</h2>
    <article>…</article>
  </section>
  <aside>Help</aside>
</main>
<footer>…</footer>

<button type="button">Save</button>  <!-- does something -->
<a href="/docs">Docs</a>             <!-- goes somewhere -->

<label for="name">Name</label>
<input id="name">

The most common gap between design and build is button versus link. If it performs an action, it is a button. If it takes you somewhere else, it is a link. They can look the same in a mockup, so say which one it is in the spec. Other elements worth knowing: <dialog>, <details> with <summary>, and tables with <thead> and <th scope="col">.

Box model and layout

  • box-sizing: border-box counts padding and border inside the width, the same way Figma does.
  • display decides how an element takes up space: block, inline, inline-block, flex, grid or none.
  • position can be static, relative, absolute, fixed or sticky. Sticky is the usual answer for a table header that stays in view.

Flexbox is Auto Layout

css

.toolbar {
  display: flex;
  flex-direction: row;            /* direction */
  gap: var(--space-2);            /* gap between items */
  padding: var(--space-3);        /* padding */
  align-items: center;            /* alignment */
  justify-content: space-between;
  flex-wrap: wrap;                /* wrap */
}
.search { flex: 1; }              /* fill container */
.button { flex: none; }           /* hug contents */
Each Flexbox property maps to an Auto Layout setting

Grid for two dimensions

css

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

.layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-areas: "sidebar main";
}
auto-fill with minmax() gives a responsive card grid without a single media query

Modern CSS worth knowing

FeatureWhat it gives a design system
Custom propertiesThe home of design tokens in code: --radius-md: 8px, then border-radius: var(--radius-md)
clamp()Fluid type and spacing with a minimum, a preferred value that follows the viewport, and a maximum
Container queriesA component adapts to the space it sits in, not to the whole page
:has()Style a parent based on what it contains, such as a field with an invalid input
Native nestingHover, focus and other states written inside the component’s own rule
Cascade layersA fixed order for reset, tokens, base, components and utilities, so specificity fights stop
Modern colouroklch(), color-mix() and relative colour syntax for tints and transparency from one token
Logical propertiesmargin-inline-start instead of margin-left, so layouts flip correctly for right-to-left languages

css

:root {
  --radius-md: 8px;
  --color-bg-brand: oklch(55% 0.2 265);
}

h1 { font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem); }

.card-wrap { container-type: inline-size; }
@container (min-width: 400px) {
  .card { flex-direction: row; }
}

.field:has(input:invalid) label { color: var(--color-text-danger); }

@layer reset, tokens, base, components, utilities;

@layer components {
  .button {
    background: var(--color-bg-brand);
    border-radius: var(--radius-md);
    padding-block: 12px;          /* top and bottom */
    padding-inline: 16px;         /* start and end */
    &:hover { background: color-mix(in oklch, var(--color-bg-brand), black 10%); }
    &:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 2px; }
  }
}
Tokens, fluid type, a container query, :has(), layers, nesting and logical properties together

Container queries matter most for a design system. The same card can sit in a narrow sidebar or a wide main area, and it changes layout based on its own width, not on a page breakpoint the component knows nothing about.

  • :focus-visible shows a focus ring for keyboard users without showing it on every mouse click.
  • aspect-ratio, inset and gap (which now works in Flexbox too) remove a lot of old workarounds.
  • The View Transitions API animates between pages or states, and @starting-style lets an element transition in when it first appears.
  • The popover attribute and anchor positioning handle menus and tooltips with little or no JavaScript. Check browser support before relying on the newest features.
  • Units: rem for type, because it respects the user’s font size; dvh for the real mobile viewport height; ch for comfortable line length.

Responsive basics

  • Mobile first: write base styles for small screens, then add @media (min-width: …) for larger ones.
  • Responsive images: srcset, <picture> and object-fit: cover.
  • User preferences: prefers-color-scheme, prefers-reduced-motion and (hover: hover) for devices that can hover.

Utility-first frameworks such as Tailwind CSS are common in teams with a design system. Its theme configuration is the token set: the designer defines the tokens, and Tailwind turns them into classes like bg-brand or px-4.

JavaScript: the parts that shape design

You don’t need to write features, but reading a few lines of JavaScript shows you where interface states come from.

js

const databases = [
  { name: 'orders-db', engine: 'postgres', status: 'Ready' },
  { name: 'cache', engine: 'redis', status: 'Provisioning' },
]

const ready = databases.filter((d) => d.status === 'Ready')
const names = databases.map((d) => d.name)

const button = document.querySelector('.copy-button')
button.addEventListener('click', async () => {
  await navigator.clipboard.writeText(connectionString)
  button.dataset.state = 'copied'   // CSS can style [data-state="copied"]
  setTimeout(() => { button.dataset.state = 'idle' }, 2000)
})
Data, array methods, and an event that changes a visual state

Common events to recognise: click, input, change, submit, keydown, focus, blur, scroll and resize. Each is a moment the design may need to respond to.

Every data screen has at least four states

js

async function loadDatabases() {
  setState('loading')
  try {
    const res = await fetch('/api/databases')
    if (!res.ok) throw new Error(res.status)
    const data = await res.json()
    setState(data.length ? 'ready' : 'empty')
  } catch {
    setState('error')
  }
}

This short function shows why a screen needs loading, ready, empty and error designs. If a designer leaves one out, a developer invents it under deadline, and every screen ends up handling it differently.

Code and Figma speak the same language

In code (Vue or React)In Figma
ComponentMain component
PropsComponent properties and variants
Slot or childrenSlot or instance swap
State (hover, open, loading)A state variant
Conditional rendering (v-if)Boolean property
Loop (v-for, .map())Repeated instances in Auto Layout
Event (@click, onClick)Prototype interaction

ts

type ButtonProps = {
  variant: 'primary' | 'secondary' | 'ghost' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  loading?: boolean
}
A TypeScript type lists exactly which variants the Figma component should offer

Constraints worth asking about

  • Performance. Very long tables need virtualisation. Heavy blur and shadow effects can be slow on phones.
  • Data. Does the API actually return the field the design shows? If not, it is backend work too.
  • Real time. Live status means polling or WebSockets. How fresh does it need to be?
  • Browser support. Check new CSS features on caniuse.com before designing around them.
  • Third-party libraries. Charts, date pickers and editors come with their own limits on customisation.
  • Text length. Translations are often longer than English, and some scripts need more line height.
  • Effort. Ask whether something is a day’s work or a week’s, and whether a simpler version gets most of the value.

Try this

Pick one component from your Figma library and write its props as a TypeScript type, like the Button above. Then open the browser DevTools on a live product and compare its CSS for the same component. Any variant in Figma that has no matching prop, or the other way round, is a gap worth raising.

Next in the series

Once a design is built, someone has to check that the build matches. Part 12 covers visual QA, writing issues engineers can act on, and turning what users actually do into design decisions.

Sources and further reading

See a design system in practice: the AppsCode Design System case study