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-boxcounts padding and border inside the width, the same way Figma does.displaydecides how an element takes up space:block,inline,inline-block,flex,gridornone.positioncan bestatic,relative,absolute,fixedorsticky. 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 */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";
}Modern CSS worth knowing
| Feature | What it gives a design system |
|---|---|
| Custom properties | The 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 queries | A 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 nesting | Hover, focus and other states written inside the component’s own rule |
| Cascade layers | A fixed order for reset, tokens, base, components and utilities, so specificity fights stop |
| Modern colour | oklch(), color-mix() and relative colour syntax for tints and transparency from one token |
| Logical properties | margin-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; }
}
}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-visibleshows a focus ring for keyboard users without showing it on every mouse click.aspect-ratio,insetandgap(which now works in Flexbox too) remove a lot of old workarounds.- The View Transitions API animates between pages or states, and
@starting-stylelets an element transition in when it first appears. - The
popoverattribute and anchor positioning handle menus and tooltips with little or no JavaScript. Check browser support before relying on the newest features. - Units:
remfor type, because it respects the user’s font size;dvhfor the real mobile viewport height;chfor 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>andobject-fit: cover. - User preferences:
prefers-color-scheme,prefers-reduced-motionand(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)
})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 |
|---|---|
| Component | Main component |
| Props | Component properties and variants |
| Slot or children | Slot 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
}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