DEV Community

Cover image for CSS Is Smarter Than You Think. Stop Babysitting It with JS
Divyam Gupta
Divyam Gupta

Posted on

CSS Is Smarter Than You Think. Stop Babysitting It with JS

You know those tiny data- things in HTML that everyone ignores? They're actually way more powerful than you think. Keep reading to see what they can really do.

Disclaimer: This article consists of pieces and excerpts from an educational conversation with Claude Haiku 4.5

CSS has evolved. You now have :has() selectors that react to your HTML, if() functions that make decisions, and data attributes that bridge everything together. Instead of JavaScript managing styles, your state lives in HTML and CSS reacts to it.

THE OLD WAY: JavaScript Managing Everything

For years, this is how we've done it. You have some state in your app—maybe a form field that's valid or invalid, a button that's loading, a card that's active or inactive. JavaScript holds that state, and whenever it changes, JavaScript updates the DOM by adding or removing classes, or changing inline styles. The browser then reads those changes and updates how things look. It works, but there's a lot of back-and-forth happening between your JavaScript and the browser's rendering engine.

// Button loading state
button.addEventListener('click', () => {
  button.classList.add('loading');
  button.disabled = true;
  // Browser reflows and repaints
});

// Form validation
input.addEventListener('change', (e) => {
  if (e.target.value === '') {
    input.classList.add('error');
  } else {
    input.classList.remove('error');
  }
  // Browser reflows and repaints
});

// Toggle dark mode
toggleBtn.addEventListener('click', () => {
  document.body.classList.toggle('dark-mode');
  // Browser reflows and repaints everything
});

// Show/hide a modal
openBtn.addEventListener('click', () => {
  modal.style.display = 'block';
  modal.classList.add('visible');
  // Browser reflows and repaints
});
Enter fullscreen mode Exit fullscreen mode

Why Should You Care?

Here's the thing: every time your JavaScript toggles a class or changes a style, your browser has to do work. It recalculates which styles apply, figures out new positions and sizes (reflow), and redraws pixels (repaint). This happens thousands of times per second in interactive apps. On a fast laptop, you might not notice. But on a phone? Your app feels sluggish.

The old approach triggers this expensive cycle repeatedly. The new approach? Your browser's CSS engine handles styling natively, which means your JavaScript thread stays free to do actual work. Less browser overhead, less lag, faster interactions.

Behavior JavaScript Managing Styles CSS Handling Natively
Reflow (recalculate layout) Triggers on every class change Only when necessary
Repaint (redraw pixels) Follows each reflow Batched with CSS engine
JavaScript thread Blocked by DOM manipulation Free for other logic
Per interaction cost 8-15ms (class toggling overhead) 2-4ms (attribute update only)
Mobile performance Drops to 25-30fps Stays at 60fps
10 rapid state changes 10 reflow cycles triggered 1 reflow cycle batched

When you toggle classes, you're asking JavaScript to talk to the DOM, which talks to the rendering engine, which reflows and repaints. When you update a data attribute, the browser's CSS engine takes over—it's optimized for this exact job.

THE NEW WAY: Let Data & CSS Do The Work

Here's where it changes. Instead of JavaScript juggling classes, you set a data attribute on your HTML element. That's it. CSS already knows what to do with it—it watches that attribute and applies the right styles automatically. Your JavaScript becomes simpler, your CSS becomes smarter, and your browser does way less work.

Example-1: Form Field Validation

Let's say you have a signup form. Users enter their email, and you need to show them if it's valid or invalid. The old way? JavaScript checks the input, adds an error class, changes the border color, shows an error message. All managed by JavaScript. The new way is simpler: you just set a data attribute, and CSS does everything else.

export default function EmailInput() {
  const [isInvalid, setIsInvalid] = useState(false);

  const handleBlur = (e) => {
    const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.target.value);
    setIsInvalid(!isValid);
  };

  return (
    <>
      <input
        className="input data-[invalid=true]:border-red-500 data-[invalid=false]:border-green-500"
        data-invalid={isInvalid}
        onBlur={handleBlur}
      />
      <span className="error-msg data-[invalid=false]:hidden">
        Please enter a valid email
      </span>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Example-2: Interactive states for buttons

Buttons have multiple states—they can be disabled, pressed, or loading. Managing all of these with conditional classes gets messy fast. With data attributes, each state is explicit and CSS handles everything. Let's look at a real button that can be disabled or pressed.

export default function Button() {
  const [isPressed, setIsPressed] = useState(false);
  const [isDisabled, setIsDisabled] = useState(false);

  return (
    <button
      className="btn data-[pressed=true]:bg-blue-700 data-[pressed=false]:bg-blue-500 data-[disabled=true]:opacity-50 data-[disabled=true]:cursor-not-allowed"
      data-pressed={isPressed}
      data-disabled={isDisabled}
      disabled={isDisabled}
      onMouseDown={() => setIsPressed(true)}
      onMouseUp={() => setIsPressed(false)}
      onClick={() => setIsDisabled(!isDisabled)}
    >
      {isDisabled ? "Disabled" : "Click me"}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Two separate data attributes, two separate concerns. JavaScript only updates state. CSS handles all the visual feedback. No class juggling, no style conflicts.

Example-3: Conditional Styling with :has()

So far we've seen data attributes controlling a single element's styles. But what if you want a parent element to change based on what's inside it? That's where :has() comes in. It lets CSS check if a child with a specific data attribute exists, and then style the parent accordingly. For example, imagine a card that should remove bottom padding only when it has a footer slot inside it.

export default function Card({ hasFooter }) {
  return (
    <div className="card has-data-[slot=card-footer]:pb-0">
      <div className="card-header">Header Content</div>
      <div className="card-body">Body Content</div>
      {hasFooter && <div data-slot="card-footer" className="card-footer">Footer Content</div>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The parent card checks if a child with data-slot="card-footer" exists—if it does, bottom padding is removed. You can also hide elements conditionally: use has-data-[slot=card-footer]:block to show content only when that slot exists. CSS reacts to what's inside without any JavaScript logic.

Example-4: Conditional Logic with CSS if()

Up until now, we've been using data attributes with Tailwind's conditional syntax. But CSS just got a new superpower: the if() function. It lets you write actual conditional logic directly in CSS. Instead of relying on pre-built Tailwind classes, you can now evaluate conditions and apply styles on the fly. For example, imagine a status badge that changes color, text, and icon based on an order status—all decided by a single if() statement in CSS.

.badge {
  --status: attr(data-status type(<custom-ident>));

  color: if(
    style(--status: shipped): #10b981;
    style(--status: pending): #f59e0b;
    else: #ef4444
  );

  background-color: if(
    style(--status: shipped): #d1fae5;
    style(--status: pending): #fef3c7;
    else: #fee2e2
  );
}
Enter fullscreen mode Exit fullscreen mode
export default function OrderStatus({ status }) {
  return (
    <div 
      className="badge"
      data-status={status}
    >
      {status === 'shipped' && '✓ Shipped'}
      {status === 'pending' && '⏳ Pending'}
      {status === 'failed' && '✗ Failed'}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

CSS if() evaluates the status value and applies colors conditionally—no JavaScript switch statements or ternary operators needed. The browser's CSS engine handles all the logic.

What's Next?

These examples are just the start. Once you think in data attributes and CSS conditionals, you'll spot opportunities everywhere—product filters, cart states, navigation menus, themes, wizards, notifications. The pattern stays the same: store state in HTML, let CSS react, keep JavaScript simple.

Pick one interactive component and try moving its styling from JavaScript to CSS. You'll feel the difference.

Top comments (1)

Collapse
 
ankita_34483fcab42b2108a6 profile image
Ankita

As a beginner in CSS, I never knew css could do THIS. Uptill now, I have only been going mad over flexbox and grids.