Build css, a small helper modeled on jQuery's .css() method, for reading and writing an element's inline styles. The same function name does three different jobs depending on how you call it: pass just a property name to read its current value, pass a name and a value to set one property, or pass an object to set several at once. Setting returns the element so calls can be chained; reading returns the value.
function css(el: HTMLElement, prop: string): string; // GET one
function css(el: HTMLElement, prop: string, value: string): HTMLElement; // SET one
function css(el: HTMLElement, props: Record<string, string>): HTMLElement; // SET many
Work only with inline styles (el.style) — never computed styles or stylesheet rules.
const el = document.createElement('div');
css(el, 'color', 'red'); // sets el.style.color, returns el
css(el, 'color'); // → 'red' (reads the inline value)
css(el, 'margin'); // → '' (unset → empty string)
// Object form sets several at once and returns the element, so calls chain:
css(el, { color: 'blue', margin: '0px' });
css(css(el, 'color', 'green'), 'padding', '4px');
'' (empty string), not undefined or null.el so you can chain further calls.el.style for both reading and writing. Do not reach for getComputedStyle or stylesheet rules.You'll write one function that inspects its own arguments and then either reads an inline style off an element or writes one (or several) back.
jQuery's .css() is overloaded: $el.css('color') reads the color, $el.css('color', 'red') sets it, and $el.css({ color: 'red', margin: '0px' }) sets a whole batch. We're rebuilding that one function for plain inline styles — the style attribute on a DOM element. The hard part isn't the DOM work (it's one property access either way); it's deciding which of the three jobs to do from the shape of the arguments you were handed.
Think of css as a switchboard. Before it touches the element, it asks two questions: "is the second argument an object?" and "was a third argument passed?" An object means set-many. A string plus a value means set-one. A string with no value means get. Only the get branch returns a value; both set branches return the element so the next call can keep chaining.
The obvious version just writes the value through and hands the element back:
function css(el, prop, value) {
el.style[prop] = value;
return el;
}
This works for css(el, 'color', 'red') and nothing else. Call it as a getter — css(el, 'color') — and value is undefined, so it runs el.style.color = undefined, which the browser coerces to ''. You wanted to read 'color', but you wiped it instead and got the element back rather than the value. The object form is missing entirely: css(el, { color: 'red' }) would do el.style['[object Object]'] = undefined, which does nothing useful. The fix is to look at the arguments first and pick a branch.
function css(el, prop, value) {
// Object form: prop is a bag of name→value pairs. Set each, then return el.
if (typeof prop === 'object' && prop !== null) {
for (const key in prop) {
el.style[key] = prop[key];
}
return el;
}
// Get form: no value was passed, so read the current inline value.
// getPropertyValue returns '' for an unset property — exactly what we want.
if (value === undefined) {
return el.style.getPropertyValue(prop);
}
// Set form: a name and a value. Write it, then return el for chaining.
el.style[prop] = value;
return el;
}
module.exports = { css };
The whole change is the two guards at the top. The first catches the object form before anything else, because typeof prop === 'object' is the only shape where prop isn't a string (the prop !== null check guards against typeof null === 'object'). The second catches the get form by testing value === undefined — if no third argument was passed, read instead of write. Only after both guards fall through do we reach the original set-one line. Note the get branch uses el.style.getPropertyValue(prop), which returns '' for an unset property; that's the contract, and it never mutates the element.
Trace css(css(el, 'color', 'red'), 'color') on a fresh <div>:
css(el, 'color', 'red'). prop is 'color' (a string, so the object branch is skipped). value is 'red' — not undefined — so the get branch is skipped too. We run el.style.color = 'red' and return el.css(el, 'color'). Same element, now prop is 'color' and value is undefined. The object branch is skipped (string), the get branch fires: el.style.getPropertyValue('color') returns 'red'.'red', and the element still has color: red — the read didn't touch it.Had we used the naive version, step 2 would have run el.style.color = undefined, blanking the color and returning the element instead of 'red'.
css(el, 'color') with no value runs el.style.color = undefined in the naive version, which clears the property and returns the element. Fix: check value === undefined and read instead of write.typeof null === 'object'. If you only test typeof prop === 'object', a null first argument would wrongly take the object branch and then for...in over nothing. Fix: also require prop !== null.el, not the value you set — otherwise chaining like css(css(el, 'color', 'red'), 'margin', '0px') breaks on the second call. Fix: return el from both set branches.getComputedStyle(el).color reads the rendered value (often rgb(255, 0, 0)), not the inline string you set, and it can't be written. Fix: read and write el.style only..css() returns the computed value when there's no inline style, so css(el, 'display') reports 'block' even if you never set it. Layering getComputedStyle in as a read-only fallback (inline first, computed second) matches that behaviour.'backgroundColor' and 'background-color'. Supporting kebab-case means routing through el.style.setProperty/getPropertyValue (which expect dashes) instead of the camelCased el.style[prop] accessor..css() across every matched element. Accepting an array (or NodeList) and looping the same get/set logic — returning the collection for chaining — generalizes this from one node to many.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build css, a small helper modeled on jQuery's .css() method, for reading and writing an element's inline styles. The same function name does three different jobs depending on how you call it: pass just a property name to read its current value, pass a name and a value to set one property, or pass an object to set several at once. Setting returns the element so calls can be chained; reading returns the value.
function css(el: HTMLElement, prop: string): string; // GET one
function css(el: HTMLElement, prop: string, value: string): HTMLElement; // SET one
function css(el: HTMLElement, props: Record<string, string>): HTMLElement; // SET many
Work only with inline styles (el.style) — never computed styles or stylesheet rules.
const el = document.createElement('div');
css(el, 'color', 'red'); // sets el.style.color, returns el
css(el, 'color'); // → 'red' (reads the inline value)
css(el, 'margin'); // → '' (unset → empty string)
// Object form sets several at once and returns the element, so calls chain:
css(el, { color: 'blue', margin: '0px' });
css(css(el, 'color', 'green'), 'padding', '4px');
'' (empty string), not undefined or null.el so you can chain further calls.el.style for both reading and writing. Do not reach for getComputedStyle or stylesheet rules.You'll write one function that inspects its own arguments and then either reads an inline style off an element or writes one (or several) back.
jQuery's .css() is overloaded: $el.css('color') reads the color, $el.css('color', 'red') sets it, and $el.css({ color: 'red', margin: '0px' }) sets a whole batch. We're rebuilding that one function for plain inline styles — the style attribute on a DOM element. The hard part isn't the DOM work (it's one property access either way); it's deciding which of the three jobs to do from the shape of the arguments you were handed.
Think of css as a switchboard. Before it touches the element, it asks two questions: "is the second argument an object?" and "was a third argument passed?" An object means set-many. A string plus a value means set-one. A string with no value means get. Only the get branch returns a value; both set branches return the element so the next call can keep chaining.
The obvious version just writes the value through and hands the element back:
function css(el, prop, value) {
el.style[prop] = value;
return el;
}
This works for css(el, 'color', 'red') and nothing else. Call it as a getter — css(el, 'color') — and value is undefined, so it runs el.style.color = undefined, which the browser coerces to ''. You wanted to read 'color', but you wiped it instead and got the element back rather than the value. The object form is missing entirely: css(el, { color: 'red' }) would do el.style['[object Object]'] = undefined, which does nothing useful. The fix is to look at the arguments first and pick a branch.
function css(el, prop, value) {
// Object form: prop is a bag of name→value pairs. Set each, then return el.
if (typeof prop === 'object' && prop !== null) {
for (const key in prop) {
el.style[key] = prop[key];
}
return el;
}
// Get form: no value was passed, so read the current inline value.
// getPropertyValue returns '' for an unset property — exactly what we want.
if (value === undefined) {
return el.style.getPropertyValue(prop);
}
// Set form: a name and a value. Write it, then return el for chaining.
el.style[prop] = value;
return el;
}
module.exports = { css };
The whole change is the two guards at the top. The first catches the object form before anything else, because typeof prop === 'object' is the only shape where prop isn't a string (the prop !== null check guards against typeof null === 'object'). The second catches the get form by testing value === undefined — if no third argument was passed, read instead of write. Only after both guards fall through do we reach the original set-one line. Note the get branch uses el.style.getPropertyValue(prop), which returns '' for an unset property; that's the contract, and it never mutates the element.
Trace css(css(el, 'color', 'red'), 'color') on a fresh <div>:
css(el, 'color', 'red'). prop is 'color' (a string, so the object branch is skipped). value is 'red' — not undefined — so the get branch is skipped too. We run el.style.color = 'red' and return el.css(el, 'color'). Same element, now prop is 'color' and value is undefined. The object branch is skipped (string), the get branch fires: el.style.getPropertyValue('color') returns 'red'.'red', and the element still has color: red — the read didn't touch it.Had we used the naive version, step 2 would have run el.style.color = undefined, blanking the color and returning the element instead of 'red'.
css(el, 'color') with no value runs el.style.color = undefined in the naive version, which clears the property and returns the element. Fix: check value === undefined and read instead of write.typeof null === 'object'. If you only test typeof prop === 'object', a null first argument would wrongly take the object branch and then for...in over nothing. Fix: also require prop !== null.el, not the value you set — otherwise chaining like css(css(el, 'color', 'red'), 'margin', '0px') breaks on the second call. Fix: return el from both set branches.getComputedStyle(el).color reads the rendered value (often rgb(255, 0, 0)), not the inline string you set, and it can't be written. Fix: read and write el.style only..css() returns the computed value when there's no inline style, so css(el, 'display') reports 'block' even if you never set it. Layering getComputedStyle in as a read-only fallback (inline first, computed second) matches that behaviour.'backgroundColor' and 'background-color'. Supporting kebab-case means routing through el.style.setProperty/getPropertyValue (which expect dashes) instead of the camelCased el.style[prop] accessor..css() across every matched element. Accepting an array (or NodeList) and looping the same get/set logic — returning the collection for chaining — generalizes this from one node to many.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.