Be Like Water - Writing Layout-Agnostic UI Components


“Be formless, shapeless, like water.” - Bruce Lee

When designing UI components, one of the most underrated skills is knowing what your component should control, and what it should delegate.

Let’s talk about a subtle but powerful pattern:
components that define their own style, but let the parent define the layout.

I like to call them water-style components.


What does “water-style” mean?

Imagine a Button component.

You want it to look consistent across your app - rounded corners, colors, hover states, typography — that’s its visual style.

But you don’t want the Button to decide:

  • where it appears on the screen
  • how much space it takes
  • whether it’s inline, flex-grow, ml-4, mt-8, or w-full

That should be decided by the parent, not the button itself.

Just like water takes the shape of the glass, these components take the shape of their container - adapting to layout context, not dictating it.


Example: Visual styling inside, layout outside

Here’s how a Button might look:

// Button.tsx
export function Button({ variant = "primary", children }) {
  const base = "rounded px-4 py-2 font-semibold text-sm";
  const variants = {
    primary: "bg-blue-600 text-white hover:bg-blue-700",
    outline: "border border-gray-400 text-gray-800 bg-white",
  };

  return <button className={`${base} ${variants[variant]}`}>{children}</button>;
}

And then we use it like this:

<div className="flex justify-end mt-8 gap-2">
  <Button variant="outline">Cancel</Button>
  <Button variant="primary">Save</Button>
</div>

The button defines:

  • Colors

  • Padding

  • Rounded corners

  • Font weight & size

But the parent defines:

  • Margin

  • Flexbox layout

  • Spacing between buttons

  • Placement on the page

Why this pattern matters

This approach gives you:

  • Reusability - use the same component in different contexts

  • Design consistency - style is centralized, not scattered

  • Layout flexibility - change positioning without modifying the component

  • Predictability - each layer has clear responsibility

It also plays perfectly with:

  • Tailwind CSS (utility-first styling)

  • Variant props (for themes or states)

  • Design systems with composable primitives

What to avoid

Don’t do this inside the component:

return <button className="w-full mx-auto my-10 text-center bg-blue-600">...</button>;

This mixes layout and style - making the button rigid, harder to reuse, and harder to maintain.

Bonus: This also works with slots

In frameworks like Vue or Svelte, this idea works beautifully with slot-based composition.

You can build layout-agnostic components that wrap any content and still adapt to layout externally.

Final thoughts

Start thinking of your components like water:

Style yourself, but don’t control the environment.

Provide your own look and feel.

Let the parent decide the placement, size, and flow.

This one mindset shift can make your component library cleaner, more reusable, and more fun to work with.

Bartłomiej Nowak

Bartłomiej Nowak

Programmer

Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.

Recent Posts