ASSA ABLOY — Technical Interview Preparation

React & CSS
The Complete Human Guide

Everything explained simply — like you're hearing it for the first time. Read this once, and the concepts will feel natural in the interview. Q&A with full answers at the end.

React Hooks React Core CSS Layout CSS Deep Concepts JavaScript
Contents

React Hooks — Explained Simply

Hooks are special functions that let you add features to a component. They always start with use. You can only call them at the top level of a component — never inside a loop, condition, or nested function.

useState — Remembering Things

Imagine your component is a person. State is that person's memory. When the state changes, the component re-renders — it "wakes up" and redraws itself with the new memory.

A digital scoreboard. Every time a team scores, the number updates and the board redraws. The current score is the state. When you call the setter, it's like pressing the "update score" button.
const [count, setCount] = useState(0); // [current value, function to change it]

// WRONG — reading count immediately after setting it won't give new value
// State updates are scheduled, not instant
setCount(5);
console.log(count); // still 0!

// RIGHT — when new value depends on old value, use a function
setCount(prev => prev + 1); // always uses the latest value

// For objects — always spread first, then override the changed field
const [user, setUser] = useState({ name: '', email: '' });
setUser(prev => ({ ...prev, name: 'Kidus' })); // keeps email intact
Never mutate state directly. user.name = 'Kidus' won't trigger a re-render. You must call the setter function. React only knows to re-render when you call the setter.

useEffect — Doing Things After Render

After React draws your component on screen, you sometimes need to do something on the side — fetch data, set up a timer, listen for a keyboard press. useEffect is where all that happens. It runs after every render, unless you tell it when to run with a dependency array.

A chef who finishes cooking (render), then after the dish is served, goes to the back to prep for the next one (side effect). The cleanup function is the chef cleaning the station before prepping the next dish.
// Runs after EVERY render — no dependency array
useEffect(() => { doSomething(); });

// Runs ONCE on mount — empty array
useEffect(() => { fetchData(); }, []);

// Runs when userId changes
useEffect(() => { fetchUser(userId); }, [userId]);

// Cleanup — the returned function
// Runs BEFORE the next effect + when component unmounts
useEffect(() => {
  const controller = new AbortController();

  fetch('/api/data', { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort(); // cancel if component unmounts mid-fetch
}, [url]);
The most common bug: a value used inside the effect that isn't in the dependency array. This creates a "stale closure" — the effect sees an old, frozen version of the value. The ESLint react-hooks plugin catches this. Always trust the linter on missing dependencies.

useCallback — Remembering a Function

In JavaScript, every time a function is written, it creates a brand new function object in memory. Even if it does the same thing, it's a different object. This matters because when you pass a function as a prop, the child component sees a "new" function every render and re-renders unnecessarily. useCallback keeps the same function reference unless the dependencies change.

Imagine you print instructions on how to make coffee every morning, even though the instructions never change. useCallback means you only reprint the instructions when the recipe actually changes — otherwise you hand the same paper.
// Without useCallback — new function every render, breaks React.memo on child
const handleClick = () => doThing(id); // new reference every render

// With useCallback — same function reference until id changes
const handleClick = useCallback(() => doThing(id), [id]);
Only use useCallback when you are passing the function to a React.memo wrapped child component, or using it in a useEffect dependency array. Adding it everywhere is worse — it adds overhead for the comparison on every render.

useMemo — Remembering a Calculated Result

If you have a calculation that is slow or expensive, you don't want to re-run it every single render. useMemo saves the result of the calculation and only recalculates when the inputs change.

A calculator that saves the last result. If you ask it the same question again, it just shows you the saved answer instead of recalculating. Only when the numbers change does it calculate fresh.
// Every render without useMemo — filters 10,000 items even if nothing changed
const filtered = items.filter(i => i.active && i.name.includes(query));

// With useMemo — only recalculates when items or query changes
const filtered = useMemo(() =>
  items.filter(i => i.active && i.name.includes(query))
, [items, query]);

// KEY DIFFERENCE from useCallback:
// useMemo   → caches the RESULT of calling a function (a value)
// useCallback → caches the FUNCTION ITSELF
// useCallback(fn, deps) === useMemo(() => fn, deps)

useRef — A Box That Doesn't Trigger Redraws

useRef gives you a container (a box) that you can put anything inside, and changing what's in the box does NOT cause a re-render. It's also the way you get direct access to a real DOM element.

A sticky note on your desk. You can change what's written on it at any time, and nobody in the room gets notified. Compared to state, which is like updating a whiteboard that everyone in the room can see — they all look up when it changes.
// Use 1: Point at a real DOM element
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []); // focus input on mount
return <input ref={inputRef} />;

// Use 2: Remember a timer ID without causing re-renders
const timerId = useRef(null);
function start() { timerId.current = setInterval(tick, 1000); }
function stop()  { clearInterval(timerId.current); }

// Use 3: Remember the previous value of something
const prevCount = useRef(count);
useEffect(() => { prevCount.current = count; }); // updates after every render

useContext — Passing Data Without Cables

Normally, to pass data from a parent to a deeply nested child, you pass it through every component in between — even the ones that don't care about it. This is called "prop drilling." Context is like a wifi signal — you broadcast data once, and any component in the tree can pick it up without wires.

// 1. Create the context (like setting up the wifi router)
const ThemeContext = createContext(null);

// 2. Provide it — wrap the components that need access
function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
}

// 3. Consume it anywhere inside the Provider — no prop drilling
function Button() {
  const { theme, setTheme } = useContext(ThemeContext);
  return <button className={theme}>Toggle</button>;
}
Every consumer re-renders when the context value changes. If you create the value object inline in the Provider (value={{ theme, setTheme }}), it creates a new object reference every render, making all consumers re-render even when nothing changed. Fix: wrap the value in useMemo.

useReducer — State With a Clear Set of Rules

useReducer is like useState but for when your state is complex and changes in many different ways. Instead of calling a setter directly, you "dispatch" an action — a named event — and a reducer function decides how state changes based on that action.

A bank teller. You don't go into the vault and grab money yourself (mutating state directly). You fill in a form describing what you want (dispatch an action), and the teller applies the rules and updates your account (reducer).
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'decrement': return { ...state, count: state.count - 1 };
    case 'reset':     return initialState;
    default: throw new Error('Unknown action: ' + action.type);
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: 'increment' });
dispatch({ type: 'reset' });
Choose useReducer over useState when: you have multiple related values that update together, the new state depends on the old state in complex ways, or you want to name your state changes explicitly (easier to read and debug).

Custom Hooks — Reusing Logic

If you find yourself writing the same stateful logic in multiple components, you can extract it into your own hook. A custom hook is just a regular function whose name starts with use. The use prefix is what tells React to apply hook rules to it.

// A hook to delay a value (useful for search inputs)
function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer); // reset the clock on each keystroke
  }, [value, delay]);

  return debounced;
}

// Now any component can use it in one line
const debouncedSearch = useDebounce(searchInput, 400);

React Core Concepts

The foundational ideas behind how React thinks, renders, and updates the screen. Understanding these helps you answer "why" questions, not just "how."

Virtual DOM — React's Draft Paper

The real DOM (the browser's version of the page) is slow to update. Every change triggers the browser to recalculate sizes, positions, and redraw. React avoids touching the real DOM unnecessarily by keeping its own lightweight copy in memory — the Virtual DOM.

An architect who draws changes on paper (virtual DOM) before touching the actual building. Instead of tearing down walls to try out a new layout, they sketch it first, compare it to the current blueprint, and only instruct workers on the exact minimal changes needed.

When your component re-renders, React makes a new virtual DOM, compares it to the old one (diffing), and applies only the specific changes that are different to the real DOM. This process is called reconciliation.

Why Components Re-render

Three things cause a component to re-render:

  • Its own state changed — you called a setState or dispatch
  • Its parent re-rendered — by default, ALL children re-render when the parent does, even if their props didn't change
  • A context it consumes changed — any subscriber to a context re-renders when the value updates
React.memo stops case #2. It wraps a component and says "only re-render if the props actually changed." But it does a shallow comparison — objects and functions created inline will always look "new" even if they have the same values.

Keys in Lists — React's ID System

When React renders a list, it needs to know which items are the same, which are new, and which were removed — especially after changes. Keys are like passport numbers for list items. They must be unique and stable.

// WRONG — using array index as key
items.map((item, i) => <Card key={i} data={item} />);
// Delete item at index 0 → all remaining items' keys shift by 1
// React thinks every item changed → unmounts and remounts everything
// Component state (input values, animations) is LOST

// RIGHT — stable unique ID from your data
items.map(item => <Card key={item.id} data={item} />);
Index as key is acceptable ONLY for static lists that are never reordered, filtered, or deleted from. For anything dynamic — always use a database ID, UUID, or any value that stays tied to that specific item regardless of position.

Stale Closure — The Hidden Bug

A closure is when a function "captures" a variable from outside it. A stale closure is when the captured variable never updates — it's frozen at the value it had when the function was created.

You write a letter and seal it. Inside you wrote "I currently have 5 euros." Even if your bank account changes next week, the letter still says 5 euros. The function is the sealed letter — it captured the value at the moment it was written.
// BUG — count is captured as 0 when useEffect is created (empty deps)
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count); // will ALWAYS print 0
    setCount(count + 1); // will ALWAYS set count to 1
  }, 1000);
  return () => clearInterval(timer);
}, []); // count not in deps → count frozen at 0

// FIX — functional update doesn't need to "see" the current count
setCount(prev => prev + 1); // always uses the latest value internally

Controlled vs Uncontrolled Inputs

Controlled: React state drives the input's value. Every keystroke updates state, state updates the input. React is always in charge.

Uncontrolled: The DOM manages its own value. You grab it when you need it using a ref.

// Controlled — React is the single source of truth
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />

// Uncontrolled — DOM is the source of truth
const ref = useRef();
<input ref={ref} defaultValue="hello" />
// read later with: ref.current.value
Prefer controlled inputs — they make validation, formatting, and syncing between fields easy. Uncontrolled is valid for file inputs (always uncontrolled) and cases where you only need the value on submit.

React Patterns & Performance

Design patterns and performance techniques that come up in mid-senior interviews. Focus especially on when NOT to use each — that's what separates candidates.

React.memo — Skip Unnecessary Re-renders

Wraps a component. Before re-rendering, React checks if the props changed (shallow comparison). If they're the same, it skips the render entirely.

const Card = React.memo(function Card({ title, onClick }) {
  return <div onClick={onClick}>{title}</div>;
});

// This breaks memo — new function reference every render
<Card onClick={() => doThing()} />;

// This works — stable reference from useCallback
const handleClick = useCallback(() => doThing(), []);
<Card onClick={handleClick} />;

Code Splitting — Load Only What You Need

By default, your entire app ships in one large bundle. Code splitting breaks it into smaller chunks that load on demand. React.lazy + Suspense handles this automatically for components.

// The Dashboard component only loads when the user navigates to /dashboard
const Dashboard = React.lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Dashboard />
    </Suspense>
  );
}

Error Boundaries — Catching Render Crashes

If a component throws an error during render, the whole app crashes. An error boundary is a safety net — it catches the error and shows a fallback UI instead. Must be written as a class component (no hook equivalent).

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true }; // switch to fallback UI
  }

  componentDidCatch(error, info) {
    logErrorToService(error); // log it somewhere useful
  }

  render() {
    if (this.state.hasError) return <h2>Something went wrong.</h2>;
    return this.props.children;
  }
}

// Wrap any risky component
<ErrorBoundary><UserProfile /></ErrorBoundary>

CSS Layout — From Scratch

Everything about how elements are positioned and sized. This is the "low level stuff" Dietrich mentioned. Know the box model cold — it underpins everything else.

The Box Model — Every Element Is a Box

Every HTML element on screen is a rectangular box made of four layers, from the inside out:

  • Content — where the text or image lives
  • Padding — space between content and the border. Has the background color.
  • Border — the visible edge
  • Margin — space outside the border. Always transparent — no background here.
A framed photo on a wall. The photo itself is content. The white matting inside the frame is padding. The frame is the border. The gap between this frame and the next one on the wall is the margin.
/* DEFAULT (content-box) — width only measures content */
.box { width: 200px; padding: 20px; border: 2px solid; }
/* Actual space on screen: 200 + 20 + 20 + 2 + 2 = 244px */
/* This surprises people — you said 200px but got 244px */

/* border-box — width INCLUDES padding and border */
*, *::before, *::after { box-sizing: border-box; }
.box { width: 200px; padding: 20px; border: 2px solid; }
/* Actual space on screen: exactly 200px — predictable! */
Always add *, *::before, *::after { box-sizing: border-box; } at the top of every project. Without it, adding padding to an element breaks your layout because it grows bigger than you specified.

Margin Collapse — When Margins Merge

When two vertical margins touch each other between block elements, they don't add together — they collapse into one margin equal to the larger of the two.

Two people standing in line. Person A needs 30cm of personal space behind them. Person B needs 20cm in front. They don't need 50cm between them — just 30cm is enough. The larger space wins.
/* margin-bottom: 30px meets margin-top: 20px → gap is 30px, NOT 50px */

/* Margin collapse does NOT happen with flex or grid children */
.parent { display: flex; } /* children's margins no longer collapse */

/* Also no collapse with padding or border between parent and child */
.parent { padding-top: 1px; } /* prevents child margin from bleeding into parent */
A child's top margin can "escape" through a parent with no border or padding — making it look like the parent has a gap above it. Fix: add overflow: hidden, padding: 1px 0, or display: flow-root to the parent.

Flexbox — Laying Things in a Line

Flexbox arranges items in one direction at a time — either a row or a column. The container controls how children are distributed along two axes: the main axis (the direction items flow) and the cross axis (perpendicular to that).

Think of seats in a cinema row. The row itself is the flex container. Each seat is a flex item. You can say "space seats evenly" (justify-content: space-between), "center seats vertically" (align-items: center), or let seats "wrap" to a new row if there are too many.
/* CONTAINER PROPERTIES */
.container {
  display: flex;
  flex-direction: row;            /* items flow left→right (default) */
  flex-direction: column;         /* items flow top→bottom */
  justify-content: space-between; /* MAIN AXIS: space, center, flex-start... */
  align-items: center;            /* CROSS AXIS: stretch (default), center... */
  flex-wrap: wrap;                /* allow items to wrap to next line */
  gap: 1rem;                      /* space between items (better than margins) */
}

/* ITEM PROPERTIES */
.item {
  flex: 1;              /* grow to fill space equally */
  flex: 0 0 200px;      /* fixed 200px — don't grow or shrink */
  flex: 2;              /* this item gets twice as much space as flex:1 items */
  align-self: flex-end; /* override parent align-items for this one item */
  order: -1;            /* move to front visually without changing HTML */
}
The main axis changes with flex-direction. With flex-direction: column, justify-content controls the vertical spacing and align-items controls the horizontal alignment — the opposite of the default row direction.

CSS Grid — Laying Things in a Table

Grid is two-dimensional — you control both rows and columns at the same time. It's like drawing a spreadsheet layout for your page. You define columns, rows, and gaps, and then place items into cells.

A city block grid. You define how many streets and avenues there are (template columns and rows). Buildings (items) can occupy one block or span multiple blocks. Some buildings are on the corner and take up two blocks wide — that's grid-column: span 2.
.grid {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;               /* 3 columns, middle is double */
  grid-template-columns: repeat(3, 1fr);            /* 3 equal columns */
  grid-template-columns: 200px 1fr 1fr;            /* fixed sidebar + fluid content */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* responsive */
  gap: 1rem;
}

.item {
  grid-column: 1 / 3;    /* from column line 1 to line 3 (spans 2 columns) */
  grid-column: span 2;   /* span 2 from wherever it lands */
  grid-row: 1 / 3;       /* span 2 rows */
}

/* Named template areas — very readable for page layouts */
.layout {
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}
.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }

auto-fit vs auto-fill

Both create as many columns as fit. The difference only shows when items don't fill the full container width.

/* auto-fit: collapses empty columns — items stretch to fill the row */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
/* 3 items on a 1000px container → 3 columns each ~333px */

/* auto-fill: keeps empty column tracks — items stay at their minimum size */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
/* 3 items on a 1000px container → 5 column slots, 3 filled + 2 empty */
Use auto-fit by default. It gives a responsive grid that fills available space cleanly. Use auto-fill only when you specifically need to preserve empty slots (e.g. a fixed-width grid with placeholder cells).

Position — Where an Element Lives in Space

position: static;   /* DEFAULT — normal flow, top/left/z-index do nothing */

position: relative; /* Stays in normal flow (space preserved) */
                    /* Can be nudged with top/left/right/bottom */
                    /* Creates a stacking context */
                    /* Children with position:absolute look to this as their anchor */

position: absolute; /* Removed from normal flow (no space reserved) */
                    /* Positioned relative to nearest positioned ancestor */
                    /* If none exists, positions relative to the viewport */

position: fixed;    /* Removed from flow, positioned relative to VIEWPORT */
                    /* Stays in place when page scrolls */
                    /* Breaks out of overflow:hidden containers */

position: sticky;   /* Relative until scroll threshold, then acts like fixed */
                    /* Stays within its scroll container */
                    /* TRAP: parent with overflow:hidden breaks sticky! */

CSS Deep Concepts

The "low level stuff" that trips people up. These are the questions that separate candidates who truly understand CSS from those who just use it.

Specificity — Who Wins When Rules Conflict

When two CSS rules target the same element and set the same property, the browser needs to decide which one to apply. It calculates a specificity score for each selector — the higher score wins.

A hierarchy of authority. A company CEO (ID selector) overrules a manager (class selector), who overrules a regular employee (element selector). Two managers of equal rank — whichever was hired last wins (source order).
/* Specificity is calculated as three numbers: (IDs, Classes, Elements) */
#header                  /* (1,0,0) — 1 ID */
.nav .link               /* (0,2,0) — 2 classes */
#header .nav a:hover     /* (1,2,1) — 1 ID + 1 class + 1 pseudo-class + 1 element */
div p                    /* (0,0,2) — 2 elements */
*                        /* (0,0,0) — universal selector contributes nothing */

/* Higher number wins, regardless of source order */
/* Same specificity → last rule declared wins */
/* !important overrides everything (avoid it in component code) */
/* Inline style beats all selectors except !important */
Avoid using IDs in CSS selectors for styling. They're so specific that they're almost impossible to override without using !important, which causes a specificity arms race. Stick to class selectors.

Stacking Context — The 3D Layer System

z-index controls which element appears on top of another. But it doesn't compare all elements globally — it only compares elements within the same stacking context. This is why z-index: 9999 sometimes "doesn't work."

Imagine layers of transparent plastic sheets. Each sheet is a stacking context. Items on the same sheet compete with each other by z-index. But a whole sheet (including all items on it) either goes above or below another sheet as one unit. A z-index of 9999 on an item won't help if its sheet is underneath another sheet.
/* A new stacking context is created by: */
position: relative/absolute/fixed/sticky + z-index (not auto);
opacity: 0.99; /* anything less than 1 */
transform: translateX(0); /* any transform */
filter: blur(0);          /* any filter */
isolation: isolate;        /* explicitly create one — best practice */
will-change: transform;

/* The problem */
.parent  { position: relative; z-index: 1; transform: translateZ(0); }
.child   { z-index: 9999; } /* trapped inside parent's stacking context */
.outside { z-index: 2; }    /* this beats .child because 2 > 1 (parent's z-index) */

/* Fix: remove transform from parent, or use isolation:isolate */

BFC — Block Formatting Context

A BFC is an isolated mini-layout environment. Elements inside a BFC are laid out independently — floats inside it are contained, and margins don't bleed out.

A room with its own rules. Furniture (floats) inside the room doesn't spill into the hallway. Noise (margin collapse) stays inside the room too.
/* Things that create a BFC */
display: flow-root;     /* purpose-built, no side effects — BEST CHOICE */
overflow: hidden;       /* common trick, clips overflowing content as side effect */
display: flex;          /* flex containers are BFCs */
display: grid;          /* grid containers are BFCs */
position: absolute/fixed;

/* Practical uses */

/* 1. Make a parent expand around its floated children (clearfix) */
.container { display: flow-root; } /* parent now wraps the float */

/* 2. Prevent margin collapse between parent and first child */
.parent { display: flow-root; }

/* 3. Two-column layout — main column doesn't wrap under floated sidebar */
.sidebar { float: left; width: 200px; }
.main    { display: flow-root; } /* creates BFC, doesn't overlap sidebar */

display: none vs visibility: hidden vs opacity: 0

display: none
Element completely removed from layout. No space. Not in accessibility tree. Triggers reflow when toggled. Like deleting it from the page temporarily.
visibility: hidden
Invisible but keeps its space in layout. Not interactive. Not in accessibility tree. Children can override with visibility: visible.
opacity: 0
Visually invisible but STILL takes up space and is still INTERACTIVE — clicks, hover, and focus still work. Still in accessibility tree.
pointer-events: none
Element is still visible and takes up space. Completely non-interactive — no hover, no click, no focus. Often paired with opacity: 0.

CSS Custom Properties (Variables)

CSS variables are defined with -- and read with var(). They are different from Sass variables because they are live — they exist in the browser, can be changed by JavaScript, cascade through the DOM, and respond to media queries.

/* Define on :root to be globally accessible */
:root {
  --brand: #0066ff;
  --spacing-md: 1rem;
}

/* Override for a specific context */
[data-theme="dark"] { --brand: #60a5fa; }
.danger-zone        { --brand: #dc2626; }

/* Use — the second argument is a fallback */
.btn { background: var(--brand, blue); }

/* Change with JavaScript */
document.documentElement.style.setProperty('--brand', '#ff0000');
document.documentElement.style.getPropertyValue('--brand');

/* Sass variables — compile-time only, static, can't be changed after build */
$brand: #0066ff; /* disappears after compilation, lives nowhere in the browser */

Units — em, rem, %, vw, vh, ch, fr

/* rem — relative to ROOT (html) font-size. Never compounds. Predictable. */
/* Best for: font sizes, consistent spacing throughout the page */
html { font-size: 16px; }
h1 { font-size: 2rem; } /* = 32px always, no matter where it is */

/* em — relative to CURRENT element's font-size. Compounds with nesting. */
/* Best for: padding/margin that should scale relative to the element's text size */
.btn { font-size: 1rem; padding: 0.75em 1.5em; } /* padding scales with font-size */

/* % — context-dependent */
width: 50%; /* 50% of parent's WIDTH */
font-size: 120%; /* 120% of parent's font-size */
padding-top: 20%; /* 20% of PARENT'S WIDTH (not height!) — common surprise */

/* vw/vh — 1% of the viewport width/height */
.hero { height: 100vh; } /* full screen height */
/* Use dvh for mobile — accounts for browser chrome that appears/disappears */
.hero { height: 100dvh; }

/* ch — width of the "0" character. Perfect for readable text columns */
article { max-width: 65ch; } /* ideal reading line length */

/* fr — fraction of available grid space, after fixed tracks are placed */
grid-template-columns: 200px 1fr 1fr;
/* 200px is placed first. Remaining space split equally into 2 columns. */

Pseudo-classes and Pseudo-elements

/* Pseudo-classes — select elements based on STATE or POSITION (:) */
a:hover, a:focus, a:active, a:visited
input:disabled, input:checked, input:required
li:first-child, li:last-child, li:nth-child(2n+1) /* odd items */
li:nth-child(3n+1) /* every 3rd item, starting from 1st */
p:not(.intro)      /* all p that don't have class .intro */

/* :is() — group selectors, takes specificity of most specific arg */
/* Instead of: header a:hover, nav a:hover, footer a:hover */
:is(header, nav, footer) a:hover { ... }

/* :has() — parent selector — style parent based on child */
.card:has(img) { padding: 0; }          /* card that contains an image */
form:has(input:invalid) { ... }        /* form that has an invalid input */

/* Pseudo-elements — create virtual elements that don't exist in HTML (::) */
p::before { content: "→ "; }   /* inserted before paragraph content */
p::after  { content: " ←"; }   /* inserted after */
::selection { background: yellow; } /* text selection color */
input::placeholder { color: gray; }
One colon vs two colons: One colon (:hover) is a pseudo-class. Two colons (::before) is a pseudo-element. Old CSS used one colon for everything — both still work, but double colon is the modern standard for pseudo-elements.

Animations and Transitions

/* TRANSITION — for state changes (hover, focus, class toggling) */
.btn {
  background: blue;
  transform: scale(1);
  transition: background 0.2s ease, transform 0.1s ease;
/*             property  duration  timing-function */
}
.btn:hover {
  background: darkblue;
  transform: scale(1.05);
}

/* KEYFRAME ANIMATION — for continuous or multi-step motion */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
/*           name duration timing  iteration-count */
}

/* PERFORMANCE RULE: Animate only transform and opacity */
/* These are composited by the GPU — no layout recalculation */
/* AVOID animating: width, height, top, left, margin, padding */
/* Those trigger full layout recalculation on every frame — janky */

Responsive Design

/* Mobile-first — write styles for mobile first, add desktop overrides */
.card { padding: 1rem; } /* mobile default */
@media (min-width: 768px) { .card { padding: 2rem; } } /* tablet+ */
@media (min-width: 1024px) { .card { padding: 3rem; } } /* desktop+ */

/* Common breakpoints */
/* 640px — small mobile landscape */
/* 768px — tablet */
/* 1024px — small laptop */
/* 1280px — desktop */

/* clamp() — fluid sizing without breakpoints */
font-size: clamp(1rem, 2.5vw, 1.5rem);
/*         minimum  fluid  maximum */

/* min-width: 0 on grid children — prevent content from overflowing */
.grid-item { min-width: 0; } /* grid items default to min-width: auto */

Semantic HTML — Why It Matters

Semantic HTML uses elements that describe what the content is, not just how it looks. This helps screen readers, search engines, and other developers understand your page without reading the CSS or JavaScript.

<!-- Wrong — no meaning, just containers -->
<div class="header"><div class="nav">...</div></div>

<!-- Right — meaning is built in -->
<header><nav>...</nav></header>
<main>
  <article>      /* self-contained content */
    <section>... /* thematic grouping */
  </article>
  <aside>...     /* related sidebar content */
</main>
<footer>...

<!-- Use button for actions, a for navigation -->
<button onclick="submitForm()">Submit</button> /* action */
<a href="/about">About</a>                  /* navigation */
<!-- Never use a div with onclick for either — no keyboard support, no role -->

Questions & Full Answers

Questions likely asked in the interview. Read each question, form your answer in your head, then read the full answer. The answers are written as you'd say them out loud — not as documentation.

React Questions
R1 What is the difference between useEffect and useLayoutEffect?

They both run side effects, but the timing is different. useEffect runs asynchronously after the browser has already painted the screen — the user sees the update before the effect runs. useLayoutEffect runs synchronously after the DOM is updated but before the browser paints — so the user never sees the intermediate state.

The practical rule is: always start with useEffect. Only switch to useLayoutEffect if you're reading DOM measurements (like getBoundingClientRect) and immediately applying a visual change based on those measurements — otherwise users see a flicker because the layout shifts after they've already seen it.

R2 What triggers a component to re-render in React?

Three things: the component's own state changing via setState or dispatch, the parent component re-rendering (which re-renders all children by default even if their props didn't change), and a context value changing.

React.memo prevents re-renders caused by the parent. It does a shallow comparison of props — so if you pass inline objects or functions as props, they'll be seen as new on every render and memo won't help. You need useCallback and useMemo to stabilize those references first.

R3 Why shouldn't you use array index as a key in lists?

Keys are how React identifies which items in a list are the same across renders. When you use index as the key, the key is tied to position, not to the actual item.

If you delete the first item, every remaining item's index shifts down by one. React sees that every key changed and remounts every element — which is expensive and destroys any local component state, like input values or scroll position. You should use a stable unique identifier from your data — a database ID, a UUID — something that stays bound to that specific item regardless of where it is in the list. Index is fine only for completely static lists that are never reordered or deleted from.

R4 What is a stale closure in React? Can you give an example?

A stale closure is when a function captures a variable at the time it was created, and then that variable updates in the future, but the function still holds the old frozen value.

The classic React example: if you create a setInterval inside a useEffect with an empty dependency array, the callback inside the interval captures count at its initial value of 0. Even after count becomes 1, 2, 3 — the interval callback still thinks count is 0. Fix: use the functional update form setCount(prev => prev + 1), which doesn't need to close over the current value.

R5 What is the difference between useMemo and useCallback?

useMemo caches the result of a function call — a value. useCallback caches the function itself. useCallback(fn, deps) is literally the same as useMemo(() => fn, deps).

Use useMemo when you have an expensive calculation you don't want to repeat on every render, or when you need to create a stable object or array reference. Use useCallback when you're passing a function as a prop to a React.memo-wrapped child component, or using a function in a useEffect dependency array. Neither should be added everywhere — they add overhead. Profile first, optimize second.

R6 What is the purpose of the cleanup function returned from useEffect?

The cleanup function runs in two situations: before the next time the effect runs again (when dependencies change), and when the component unmounts from the DOM.

It exists to prevent memory leaks and stale updates. Without cleanup, a timer could keep firing after the component is gone. A fetch could try to update state on an unmounted component. An event listener could accumulate duplicates. The most important cleanup patterns are: calling clearInterval or clearTimeout for timers, calling controller.abort() on an AbortController to cancel in-flight fetch requests, and removing event listeners with removeEventListener.

R7 When would you use useReducer instead of useState?

I'd reach for useReducer when the state has multiple sub-values that update together, when the next state depends on the previous state in complex ways, or when there are many different "events" that can change the state. Having named action types like 'add_item', 'remove_item', 'reset' makes the code much more readable and easier to debug than multiple setState calls scattered around.

It also pairs naturally with useContext — you can put the dispatch function in context and let any component trigger state changes without prop drilling.

R8 What is prop drilling and how do you solve it?

Prop drilling is when you need to pass data through several layers of components that don't actually need it, just to get it to a deeply nested component that does. Every middle component becomes a "pipe" — it receives the prop only to pass it down.

Solutions in order of preference: first, check if the state can be colocated closer to where it's actually used — that's often the real fix. Second, use component composition — pass the consumer component as a child instead of drilling data. Third, use Context API for genuinely global data like the current user or theme. Fourth, use a state management library like Zustand or Redux Toolkit for complex shared state with many consumers.

R9 What is the Virtual DOM and what is reconciliation?

The Virtual DOM is a lightweight JavaScript representation of the real DOM that React keeps in memory. When your component renders, React produces a new virtual DOM tree. Reconciliation is the process of comparing the new virtual DOM to the previous one, figuring out the minimum set of changes needed, and then applying only those changes to the real DOM.

This is important because real DOM operations are expensive — they trigger layout calculations and screen repaints. By batching and minimizing DOM operations through reconciliation, React makes UI updates much faster. Keys in lists are part of this — they help React correctly match items across renders instead of throwing everything away and rebuilding.

R10 What is the difference between controlled and uncontrolled components?

A controlled component is one where React state is the single source of truth for the input's value. Every keystroke updates state, and state drives what the input displays. You have full control — you can validate, transform, and respond to each change in real time.

An uncontrolled component lets the DOM manage its own state. You use a ref to read the value when you need it, typically on submit. It's simpler to set up but you lose the ability to respond to changes as they happen. File inputs are always uncontrolled — there's no React-controlled way to set the file value for security reasons.

CSS Questions
C1 Explain the CSS box model. What does box-sizing: border-box do?

Every element is a rectangular box made of four layers from inside out: content, padding, border, and margin. The default box-sizing is content-box, which means the width property only sets the content area. If you add padding and border, the element becomes wider than the width you specified.

border-box changes the calculation so that width includes the content, padding, and border together. If you say width: 200px, the element is exactly 200px including its padding and border. This is much more intuitive, which is why every project starts with *, *::before, *::after { box-sizing: border-box; }.

C2 What is margin collapse? When does it NOT happen?

Margin collapse is when two vertical margins between adjacent block elements merge into one margin equal to the larger of the two — not their sum. Two siblings with 30px bottom and 20px top margin will have a 30px gap between them, not 50px.

It does NOT happen between flex children, grid children, elements with overflow set to anything other than visible, or when there's a border or padding between a parent and its first/last child. The most common surprise is a child's top margin "escaping" through a parent — appearing as if the parent has a gap above it. Fix it with display: flow-root, a small padding on the parent, or overflow: hidden.

C3 How does CSS specificity work?

Specificity is a scoring system that decides which CSS rule wins when multiple rules target the same element. Scores are calculated across three buckets: ID selectors count in the highest bucket, class selectors, attribute selectors, and pseudo-classes count in the middle bucket, and element selectors and pseudo-elements count in the lowest bucket. Higher score wins regardless of where the rule appears in your CSS. If scores are equal, the last-declared rule wins.

Inline styles beat all selectors. !important beats everything and should be avoided in component styles because it makes overriding almost impossible. The practical rule: avoid ID selectors in your CSS — use classes instead. They're much easier to work with because they sit in the same specificity bucket.

C4 Why does z-index sometimes not work?

Because z-index only compares elements within the same stacking context. A stacking context is an isolated layer — elements inside it stack relative to each other, but as a group they compete with other stacking contexts.

The problem: if an element with z-index: 9999 is inside a parent that created a stacking context at z-index: 1, the whole parent group competes with z-index: 1 — regardless of what the child's z-index is. The fix is to find which ancestor is creating the stacking context and either remove the property causing it (usually transform or opacity < 1), or increase the parent's own z-index. Using isolation: isolate is the clean way to intentionally create stacking contexts without side effects.

C5 What is a BFC and why would you use one?

A Block Formatting Context is an isolated layout environment where internal layout doesn't interact with the outside. Inside a BFC, floats are contained (the BFC expands to wrap them), margins don't collapse between parent and child, and the box doesn't overlap floats from outside.

The most useful ways to create one: display: flow-root (purpose-built, no side effects), overflow: hidden (common trick, but clips overflowing content), or flex and grid containers automatically create one. Common uses: making a parent expand around floated children without clearfix hacks, preventing a child's margin from escaping into the parent, and creating a two-column layout where the main column doesn't wrap under a floated sidebar.

C6 Explain the difference between display: none, visibility: hidden, and opacity: 0.

display: none removes the element from the layout completely — it takes up no space, isn't visible, can't be interacted with, and is invisible to screen readers. Toggling it triggers a reflow because the browser has to recalculate surrounding layout.

visibility: hidden hides the element but keeps its space in the layout. It's not visible and not interactive, but the space it occupied remains. Children can be made visible with visibility: visible.

opacity: 0 makes the element visually invisible, but it still takes up its full space and — importantly — is still fully interactive. You can still click, hover, and focus it. It's also still readable by screen readers. This catches people off guard because invisible elements are still clickable.

C7 What is the difference between em, rem, and %?

rem is relative to the root font-size — the html element's font-size, usually 16px. It never compounds. Whether you write it on a top-level component or a deeply nested one, 1rem is always 16px. It's the most predictable unit, and I use it for font sizes and spacing throughout most of a project.

em is relative to the current element's own font-size. If used for font-size itself, it's relative to the parent's font-size. It compounds with nesting — a child at 1.2em inside a parent at 1.2em ends up at 1.44x the root size. It's useful for padding and margins that should scale with the element's own text size.

Percentage is context-dependent. For width, it's a percentage of the parent's width. For font-size, it's a percentage of the parent's font-size. The surprising one: vertical padding and margin (top and bottom) as a percentage are calculated relative to the parent's WIDTH, not height. That's a common source of unexpected layout behavior.

C8 What is the difference between Flexbox and Grid? When do you use each?

Flexbox is one-dimensional — it lays items out in a single row or column. Grid is two-dimensional — you control rows and columns simultaneously. That's the core distinction.

I use Flexbox when I'm arranging items in a line — a nav bar, a row of buttons, a card with an icon and text side by side. I use Grid when I'm designing a layout — a page structure with header, sidebar, and main content, or a grid of cards that need to align both horizontally and vertically. In practice, they complement each other. A page uses Grid for the overall layout, and each section uses Flexbox for its internal content. Neither replaces the other.

C9 What is position: sticky and what is the common trap with it?

Sticky is a hybrid — an element behaves like position: relative until it reaches a scroll threshold (like top: 0), then it "sticks" and behaves like position: fixed within its scroll container. It goes back to relative behavior once the parent scrolls out of view.

The most common trap is a parent element with overflow: hidden or overflow: auto. This breaks sticky because sticky needs a visible scroll container to stick within. If any ancestor has overflow set to anything other than visible, sticky silently stops working. The fix is to find that ancestor and either remove the overflow property or restructure the HTML so the sticky element's scroll container doesn't have overflow restrictions.

C10 What are CSS custom properties and how are they different from Sass variables?

CSS custom properties (also called CSS variables) are defined with a double dash prefix and read with var(). The fundamental difference from Sass variables is that custom properties are live — they exist in the browser at runtime. They cascade and inherit through the DOM, can be overridden by media queries or specific selectors, and can be read and changed with JavaScript dynamically.

Sass variables are compile-time only. After compilation, they don't exist in the browser — they've been replaced with their values in the output CSS. This means they can't respond to user interactions, theme changes, or JavaScript. For a dark mode that switches at runtime, you use CSS custom properties — Sass can't do that.

C11 What are all the CSS position values and how are they different?

static is the default. The element is in normal document flow. top, left, and z-index have no effect.

relative keeps the element in the normal flow but lets you offset it with top/left/right/bottom. Crucially, it creates a stacking context and acts as the positioning anchor for any absolutely positioned children.

absolute removes the element from the normal flow — it takes up no space in the document. It's positioned relative to the nearest ancestor that has a position other than static. If no such ancestor exists, it positions relative to the viewport.

fixed is like absolute but always positioned relative to the viewport. It doesn't scroll with the page. It also breaks out of overflow: hidden containers.

sticky is relative until a threshold, then fixed within its scroll container. It returns to relative when the parent scrolls away. Broken by parent overflow properties.

C12 What is auto-fit vs auto-fill in CSS Grid?

Both create as many columns as fit given a minimum size. The difference only matters when the items don't fill the full row.

With auto-fit, empty column tracks are collapsed to zero width — existing items stretch to fill the entire container. With auto-fill, empty column tracks are preserved at their minimum size — items don't stretch to fill the gap.

In practice, auto-fit is the right choice almost always. It gives a responsive card grid that fills the available space cleanly. Use auto-fill only when you specifically want to preserve those empty slots — like a calendar grid where you need a consistent number of columns regardless of how many items there are.

C13 How do CSS transitions and animations work? What's the performance rule?

Transitions animate a property from one value to another when a state change happens — like hovering or toggling a class. You declare which property to animate, how long it takes, and what timing function to use. The animation happens automatically whenever that property changes.

Keyframe animations use @keyframes to define a multi-step animation and animation to apply it. You control name, duration, timing, and iteration count. These run continuously or for a fixed number of cycles.

The performance rule: only animate transform and opacity. These properties are handled by the GPU compositor — they don't require the browser to recalculate layout or repaint pixels. Animating width, height, top, left, or margin triggers a full layout recalculation on every frame — which causes jank, especially on lower-end devices.