Implement jQuery css()
Prompt
Build a small version of jQuery's css() method. You expose a function jq(selector) that returns an object with a css method. The method is overloaded: the same name reads styles in some calls and writes styles in others. The shape of the arguments tells you which.
<div> <p id="test" style="color: red; opacity: 1;"> Hello World </p></div>Read one property (from the first matched element):
jq('#test').css('color');// "rgb(255, 0, 0)"Read several properties at once:
jq('#test').css(['color', 'opacity']);// { color: "rgb(255, 0, 0)", opacity: "1" }Set one property on every matched element:
jq('#test').css('color', 'green');Set per-element via a function:
function setColor(index, currentValue) { return index % 2 === 0 ? 'orange' : 'green';}jq('p').css('color', setColor);If the function returns undefined for an element, that element is left alone.
Set several properties via an object:
jq('p').css({ color: 'red', opacity: 1 });The set forms return the chain object so calls can be chained: jq(el).css('color', 'red').css('opacity', 0.5).
Requirements
css(propertyName)returns the computed value of the property for the first matched elementcss([propertyName, ...])returns an object with each requested property as a key and its computed valuecss(propertyName, value)writes the value to every matched element's inline stylecss(propertyName, fn)callsfn(index, currentValue)for each matched element with the element bound asthis;undefinedfrom the function leaves that element unchangedcss(properties)writes every key of the object to every matched element's inline style- Set calls return the chain so
jq(el).css(a).css(b)works - If the selector matches no elements, the get forms return
undefinedor an empty object, and the set forms are a no-op that still returns the chain
Playground
Start by collecting the matched elements. If the argument
is a string, use document.querySelectorAll. Wrap the
result in an array so you can map and iterate freely.
Dispatch on the shape of args inside css. One string:
get single. One array: get many. One object: set many. Two
arguments: set single (value or function). This is the
main structure of the whole solution.
For get, read with
getComputedStyle(el).getPropertyValue(prop). For set,
write with el.style.setProperty(prop, String(value)).
Always return the chain object from a set so callers can
chain.
Solution
Explanation
The whole problem is pattern matching on the arguments. Once you see that, each branch is short.
Collect the elements first
jq takes a selector and produces an object that wraps a list of elements. querySelectorAll returns a NodeList, so convert it to an array. That way we can use forEach, for...of, and map naturally.
const elements = typeof selector === 'string' ? Array.from(document.querySelectorAll(selector)) : [selector];Accepting a raw element in addition to a string is a small convenience that mirrors real jQuery. jq(elementRef).css(...) works without writing a unique selector first.
One method, five call shapes
Inside css, the argument shape determines what to do. Handle the cases in a fixed order:
- One argument, string → get a single value.
- One argument, array → get multiple values and return an object.
- One argument, plain object → set every property on every element.
- Two arguments, second is a function → set per element using the function's return value.
- Two arguments, second is anything else → set the value on every element.
The order matters only between (1) and (2), since typeof 'x' === 'string' and Array.isArray(['x']) do not overlap. It does matter to check (3) after (2), because arrays are also typeof 'object'.
Get uses computed styles
getComputedStyle(el) returns the browser's final, cascaded style for the element. It reads both inline styles and stylesheet rules. getPropertyValue('color') gets the value of a single property and works with kebab-case names.
if (args.length === 1 && typeof args[0] === 'string') { if (elements.length === 0) return undefined; return getComputedStyle(elements[0]).getPropertyValue( args[0] );}The array form iterates the list of names and builds a plain object:
if (args.length === 1 && Array.isArray(args[0])) { const result = {}; if (elements.length === 0) return result; const styles = getComputedStyle(elements[0]); for (const prop of args[0]) { result[prop] = styles.getPropertyValue(prop); } return result;}Only the first element is read. That matches jQuery: there is no sensible way to collapse three different colors into one return value, so the first element wins.
Set writes to inline styles
Get reads the cascade, but set writes only to the element's inline style. el.style.setProperty takes kebab-case names and accepts any string value.
if ( args.length === 1 && args[0] !== null && typeof args[0] === 'object') { const properties = args[0]; for (const el of elements) { for (const prop in properties) { el.style.setProperty(prop, String(properties[prop])); } } return chain;}Wrapping the value in String(...) handles numeric inputs like opacity: 1 or opacity: 0.5 without a special case.
The function form
The function gets the index of the element and the current computed value of the property, with this bound to the element itself. Whatever it returns becomes the new inline value. If it returns undefined, do nothing for that element. Using value.call(el, index, currentValue) is what makes the this binding work; plain value(index, currentValue) would leave this unset in strict mode.
if (args.length === 2) { const [prop, value] = args; elements.forEach((el, index) => { if (typeof value === 'function') { const currentValue = getComputedStyle(el).getPropertyValue(prop); const newValue = value.call(el, index, currentValue); if (newValue !== undefined) { el.style.setProperty(prop, String(newValue)); } } else { el.style.setProperty(prop, String(value)); } }); return chain;}The undefined check is the one subtle part. It lets the caller conditionally skip elements without a second loop. Returning the old value would also work, but that extra write would fire mutation observers and is easy to get wrong for properties that have side effects.
Why set returns the chain
Every set branch ends with return chain. Without it, the second .css(...) in jq(el).css('color', 'red').css('opacity', 0.5) would run on undefined. Returning the same object keeps the API fluent and matches every other jQuery method.
A common mistake is to forget that arrays are also
typeof 'object'. If the object-set branch runs before
the array-get branch, css(['color']) would be treated as
a set with { 0: 'color', length: 1 }. Either check the
array case first, or use args[0].constructor === Object
to match plain objects only.
Small extensions
- Numeric units. Real jQuery appends
pxto numeric values for length properties likewidth,height,top, andmargin-*. A maintainedLENGTH_PROPSset and a small check in the set path covers this. - CamelCase property names. jQuery accepts
backgroundColoras well asbackground-color. Convert with a small regex:prop.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()). - Removing a property. Passing an empty string should remove the inline style. The current code already does this because
setProperty(prop, '')clears the property.