jackstack

UI Development Core Principles

When you’ve been doing any sort of frontend development for an extended period of time you come to understand a few bits of wisdom. This post is my attempt to articulate some ideas and concepts I’ve internalized over the years.

Frontend Development is Hard

Let me start by saying that anyone can add some elements to a page. But it is both a science and an art to make components, layouts, and flows look and feel good while keeping the code maintainable.

As a “full-stack” engineer, getting a component styled and behaving exactly how you want mirrors the complexities and engineering effort required to plug in a module in a data system, optimizing a slow part of an algorithm, or improving query time.

I’ve met several engineers who shy away from frontend development because they feel that asking them to learn css or apply styles is somehow insulting their intellect, as if it were beneath them. My challenge to you, reader, is to embrace frontend development. Making things look and feel good is very rewarding.

Functional Components by Default

The responsibility and scope of components should be as minimal as possible. In general, you have a good functional component when it does one thing, does it well, and can be reused with little cognitive overhead.

Components should be portable and reusable. They should be independent of any external layout or state. I like to think of them as little blocks that you can slot in as needed with their functionality and appearance remaining intact.

Layouts, Spacing, and Ownership

When you're creating functional components, at first it might seem intuitive to also define the margin of that component inside of the component's definition. But what happens when you want to use that same component in a different layout? Now you'll most likely have to remove the margin from the component's definition, have the parent control it, and then you'll have to fix all previous definitions where that component was used. Hope you don't miss one!

For this reason, the parent layout or container should manage the spacing and margin of its children and components should be margin agnostic by default. They absolutely can manage the padding and spacing of its internal content but should not be concerned with external spacing.

Let's look at a basic example of what happens when a component manages its margin. Consider the following functional component in phoenix/elixir.

def my_component(assigns) do
 ~H"""
  <div class="mt-3"> ... </div>
 """
end

Now assume my_component is rendered in a layout like below.

<div class="p-2">
 <div class="mt-2" />
 <.my_component /> <!-- Has top margin -->
</div>

At first glance, you wouldn't know that my_component has margin applied to it!

The layout might still work but you wouldn't know about the component's margin unless you looked at the code of the component. A better solution would be to remove the margin from the component's definition and have the layout fully manage it.

<div class="p-2">
 <div class="mt-2" />
 <.my_component class="mt-2" />
</div>

At a single glance you can understand the spacing relationships of the layout. This is more straightforward and easier to read.

On that note, I prefer semantic spacing as opposed to repeatedly adding margins to children.

<div class="space-y-2">
 <h1>Foo Bar</h1>
 <div>
  ...
 </div>
 <.my_component />
</div>

If, for whatever reason, you need to explicitly define margins on individual elements, prefer to add them "reactively" where each child's spacing is "reacting" to the existing layout structure defined before it.

<div>
 <h1>Foo Bar</h1>
 <div class="mt-2">
  ...
 </div>
 <.my_component class="mt-2"/>
</div>

An alternative might look like:

<div>
 <h1 class="mb-2">Foo Bar</h1>
 <div class="mb-2">
  ...
 </div>
 <.my_component />
</div>

But this suggests the preceding element should manage the next element's spacing, which feels a bit weird.

Composition Over Configuration

Prefer multiple smaller components with few configurations as opposed to large monolithic components with an overwhelming amount of options. This allows you to make more scoped changes over time and it allows you to swap around or adjust the composition as needed.

Initially though, I tend to start out with a general idea for a larger “outer” component. I’ll start by naively shoving everything into that outer component and let the "cut points", or logical separations, reveal themselves to me. By the time a PR is created, I will likely have created a single conceptual component with multiple smaller components that help “compose” the concept.

<!-- undesirable -->
<.card 
  title_size="lg"
  title_text="My card"
  use_divider={true}
  body_padding="4"
  body_content="Card body content here"
/>

<!-- a bit better -->
<.card>
  <.card_title size="lg">My card<.card_title>
  <.card_divider />
  <.card_body class="p-4">
   Card body content here
  </.card_body>
</.card>

Know Your Bounds

I'll often add temporary borders to elements when trying to familiarize myself with a layout, figure out where new elements and components exist on the dom, or when trying to figure out why a layout behaves a certain way. It's also helpful when pair-programming with colleagues.

This is a simple yet helpful visualization technique that persists between renders due to code changes.

For example:

<div class="flex justify-between border border-red-500">
  <div class="border border-green-500">..</div>
  <div class="border border-blue-500">..</div>
</div>

When you have complex layouts, this can be pretty useful.

Screenshot 2025-08-18 at 7

State Management

State should be as close to where it’s used as possible. State defined too far up the tree adds bloat and confusion, state defined not far enough causes out-of-sync issues or redundant code.

Be mindful about how far you’re prop drilling1 something. Each child you drill to adds another layer that a contributor must peel back; each additional layer adds a surprising amount of cognitive overhead.

Make use of derived state as much as possible. Even if some computations are redundant, I’d prefer to stay in sync rather than have a minor performance gain. Derived state also allows you to remain flexible in early development as you’re not shackled to an existing shape or structure in the store, you can modify the shape as needed without breaking downstream consumption. Derived state can serve you while you build intuition.

Custom components

Don't be afraid to make custom components. This is inevitable once you’re implementing non-trivial designs, ideas, and functionality. I've rarely encountered component libraries that behave exactly the way teams want them to.

When implementing custom components, or even standardizing2 components, I typically think to myself "What would this look like if it was part of an external library?". This way, you can generalize the code and behavior of the components. When I need to create a custom component, I'll first enumerate what I think some practical usages of this component might look like.

Pixel Perfection

When working with a designer, aspire to be “pixel perfect.” In other words, your implementation should aim to be an exact replica of the designs. That is, the colors, font sizes, spacing, everything, should match the design files.

It is trivial to identify the exact properties of a component in standard design tools like Figma. Some of these tools even generate code that you can reference. If you’re working with a great designer, it’s easier to get close to pixel perfection.

Pixel perfection isn’t always possible. Sometimes there are edge cases or incompatibilities. So if you can’t meet pixel perfection, you should be able to explain why and communicate why you made certain calls.

Laziness or “I don’t know how to do that” or “It was faster for me to do it this way” are invalid reasons for missing pixel perfection and do not qualify for reviews by the designer.

  1. Prop drilling refers to the act of passing data through several layers of intermediate components, even if those components don't use that data. This is a pretty infamous concept in React.js but it's applicable to Elixir & Phoenix LiveView components.

  2. Even if you're using a really good component library, it's a good idea to standardize your team's usage of the library. Usually you'll have your own "wrappers" around existing components.