Tailwindcss: [Feature Proposal] Stack utilities

Created on 16 Apr 2020  Â·  10Comments  Â·  Source: tailwindlabs/tailwindcss

It is common to want a border _between_ each of the elements in a list. The Tailwind documentation recommends adding borders to each element but the first or last, using the :first-child variant:

<div>
  <div v-for="item in items" class="border-t first:border-t-0">
    {{ item }}
  </div>
</div>

By using the "lobotomized owl" selector, as introduced here and elaborated on here, it is possible to write HTML that looks like this:

<div class="stack-border-y">
  <div v-for="item in items">
    {{ item }}
  </div>
</div>

The border class is moved from the component elements to the surrounding context. This reduces the number of classes necessary. To me, it also conveys the intention of the design better: "this is a vertical stack of elements with a border between each of them."

This is applicable to Tailwind UI. Obviously, it applies to bordered lists. In the case of panels/cards, it is especially useful in procedurally generated HTML, when a panel will _sometimes_ have multiple sections, but other times not.

Margin and padding are very similar—often you want the same amount of space _between_ a series of n elements. This is most easily configured on the context surrounding the elements, rather than on each element but the first or last. Otherwise, you can accidentally end up with different amounts of space between elements, or with spacing bugs when the order of elements is switched.

To support these use cases, I've been adding the following stack-* plugins to my projects. I propose they could be promoted to core plugins.

{
    plugins: [
        plugin(function ({ addUtilities, e, theme, variants }) {
            const utilities = _.flatMap(theme('padding'), (size, modifier) => ({
                [`.${e(`stack-my-${modifier}`)} > * + *`]: { marginTop: `${size}` },
                [`.${e(`stack-mx-${modifier}`)} > * + *`]: { marginLeft: `${size}` },
                [`.${e(`stack-py-${modifier}`)} > * + *`]: { paddingTop: `${size}` },
                [`.${e(`stack-px-${modifier}`)} > * + *`]: { paddingLeft: `${size}` },
            }));
            addUtilities(utilities, variants('padding'));
        }),
        plugin(function ({ addUtilities, e, theme, variants }) {
            const generator = (value, modifier) => ({
                [`.${e(`stack-border-y${modifier}`)} > * + *`]: { borderTopWidth: `${value}` },
                [`.${e(`stack-border-x${modifier}`)} > * + *`]: { borderLeftWidth: `${value}` },
            });
            const utilities = _.flatMap(theme('borderWidth'), (value, modifier) => {
                return generator(value, modifier === 'default' ? '' : `-${modifier}`);
            });
            addUtilities(utilities, variants('borderWidth'));
        }),
    ],
}

If you'd like, I'd be willing to put together a PR.

Most helpful comment

[A variant] feels a little more "Tailwind-y" in some sense that it's not trying to be as cute of an abstraction and is instead more direct

Agreed, one of the things I like about Tailwind is the lack of cuteness. A variant would be more direct. That said, I shied away from a variant in this case because you would have to know the implementation to use it correctly. For example, a variant would generate between:border-b, but using that would (almost always) be a mistake. The right way to use it would be between:border-t. That's why I chose to use the x and y mnemonics, which strays from the variant path.

To me, a variant also seems like overkill. Besides spacing and borders, I can't imagine what else you would put it on. There are a few other "directional" utilities like .rounded and .inset, but I haven't yet found the need to adjust the border-radius _between_ elements.

I'm not sure where to specify the border color

I've wrestled with that question too, and not found a good answer.

Up until now, I've been putting the border color on the children. That's been good enough for me because I rarely have a list whose borders are sometimes gray and sometimes green. This is a question of whether the children are truly context free. I guess I care that the border's _presence_ is context free, but not its _color_.

@hacknug, thanks for the pointer to benface/tailwindcss-children. If the need ever arises, I'll pull that in to use children:border-gray-500.

what the class should be called

I'm not crazy about stack either. I like divide, especially the idea of combining border position and color as divide-x-{size}, divide-y-{size} and divide-{color}. For margins, space-x-{size} is OK, though it would preclude padding helpers.

If it's definitely going to be a variant, I don't love between:. As @hacknug points out, there's no such thing as the font size "between" two children.

All 10 comments

This is something I think about a lot, and I use similar custom spacing utilities in my own projects (we actually use them on the Tailwind UI website, called space-x-{n} and space-y-{n}).

I've been wanting to add this to Tailwind for a while but the border problem has made me hesitate, mainly because I'm not sure where to specify the border _color_ and what the class should be called.

I don't personally love the stack-border-y naming convention but also don't have any really great alternatives

One alternate approach I considered was a between: variant that used the owl selector, so you could do stuff like:

<div class="between:mt-4">
  <!-- ... -->
</div>

...or:

<div class="between:border-t between:border-gray-500">
  <!-- ... -->
</div>

I feel like I thought of a deal-breaking flaw in that solution at some point but can't remember it off-hand. It feels a little more "Tailwind-y" in some sense that it's not trying to be as cute of an abstraction and is instead more direct, but I'm not really convinced one way or another.

For borders we could even reserve a new namespace, like divide-y or divide-x, and then divide-gray-500 for the color. If we did something like that and space-y-4 and space-x-4 it would feel fairly consistent at least in the sense that both have new names.

As I just mentioned on Twitter, imho this should be introduced as a variant. I’ve been using an owl variant plugin in all of my projects for a while and it does wonders (thank you Heydon for the awesome name).

To give a little context, it started as a fork of this other plugin. Reached out to the author suggesting changing it to a variant to close all of the issues opened at that time (haven’t checked now if there’s any new one). He said he’d think about it but never got back to me.

Then one day I brought it up on Discord and @benface decided to rename it and add to his tailwindcss-children variants plugin. Here’s a link to the relevant issue: https://github.com/benface/tailwindcss-children/issues/5

——

Should be a variant because it can be and will let everyone use it in other contexts we’re probably not thinking of right now. Why should that require writing more plugins and maybe even adding more stuff in core?

Regarding the border color question what I do is add that to the children or use Ben’s children variant to keep it on the parent element. By adding it to the children you can do cool things like rainbow/gradient border scale (just a silly example that someone might actually need to implement one day). Maybe in this case core should also add children?

The deal-breaking flaw might have been related to variant stacking (ie responsive + another one) or something like prefix or important. I haven’t really tested against all of core’s features but didn’t encountered any issues with the things I tried.

——

Name wise I still think owl is great because it’s short and describes what’s going to happen.

between works well for margin but not for other styles because you’re only skipping the first element.

children:not-first makes a lot of sense in the context of Ben’s plugin but I feel like it’s too long and verbose (imagine a website with 5 breakpoints and changing the space between items for each of them).

[A variant] feels a little more "Tailwind-y" in some sense that it's not trying to be as cute of an abstraction and is instead more direct

Agreed, one of the things I like about Tailwind is the lack of cuteness. A variant would be more direct. That said, I shied away from a variant in this case because you would have to know the implementation to use it correctly. For example, a variant would generate between:border-b, but using that would (almost always) be a mistake. The right way to use it would be between:border-t. That's why I chose to use the x and y mnemonics, which strays from the variant path.

To me, a variant also seems like overkill. Besides spacing and borders, I can't imagine what else you would put it on. There are a few other "directional" utilities like .rounded and .inset, but I haven't yet found the need to adjust the border-radius _between_ elements.

I'm not sure where to specify the border color

I've wrestled with that question too, and not found a good answer.

Up until now, I've been putting the border color on the children. That's been good enough for me because I rarely have a list whose borders are sometimes gray and sometimes green. This is a question of whether the children are truly context free. I guess I care that the border's _presence_ is context free, but not its _color_.

@hacknug, thanks for the pointer to benface/tailwindcss-children. If the need ever arises, I'll pull that in to use children:border-gray-500.

what the class should be called

I'm not crazy about stack either. I like divide, especially the idea of combining border position and color as divide-x-{size}, divide-y-{size} and divide-{color}. For margins, space-x-{size} is OK, though it would preclude padding helpers.

If it's definitely going to be a variant, I don't love between:. As @hacknug points out, there's no such thing as the font size "between" two children.

This is something I think about a lot, and I use similar custom spacing utilities in my own projects (we actually use them on the Tailwind UI website, called space-x-{n} and space-y-{n}).

I've been wanting to add this to Tailwind for a while but the border problem has made me hesitate, mainly because I'm not sure where to specify the border _color_ and what the class should be called.

I don't personally love the stack-border-y naming convention but also don't have any really great alternatives

One alternate approach I considered was a between: variant that used the owl selector, so you could do stuff like:

<div class="between:mt-4">
  <!-- ... -->
</div>

...or:

<div class="between:border-t between:border-gray-500">
  <!-- ... -->
</div>

I feel like I thought of a deal-breaking flaw in that solution at some point but can't remember it off-hand. It feels a little more "Tailwind-y" in some sense that it's not trying to be as cute of an abstraction and is instead more direct, but I'm not really convinced one way or another.

For borders we could even reserve a new namespace, like divide-y or divide-x, and then divide-gray-500 for the color. If we did something like that and space-y-4 and space-x-4 it would feel fairly consistent at least in the sense that both have new names.

I implemented this in my personal sass Library, PetricorCSS, which is inspired by Tailwind css.
I use the same (more o less) naming convention and I use the lobotomized owl as a variant.

Border width stack variant

| Html class | Css class | Property |
| --------------------- | -------------------------- | ----------------- |
| stack:border-t{-size?} | .stack\:border-t{-size?} > * + * | border-top: $rem |
| stack:border-l{-size?} | .stack\:border-l{-size?} > * + * | border-left: $rem |

Margin stack variant

| Html class | Css class | Property |
| --------------------- | -------------------------- | ----------------- |
| stack:mt-{size} | .stack\:mt-{size} > * + * | margin-top: $rem |
| stack:ml-{size} | .stack\:ml-{size} > * + * | margin-left: $rem |

The stack variant is applied in a series of elements, be it vertical or horizontal. For this reason I only apply this to the top or left properties when working with borders or margins. This is what I do, but it could be applied to all sides.

Note: Notice this is applied to the parent element instead of children.

This is how implement this variant in my SASS library

Usage

<ul class="list-none pl-0 mb-0 | stack:border-t stack:border-gray-200">
  <li>Item 1</li>
  <li>Item 2</li>
  <li class="border-t-0">Item 3</li> <!-- Overwrite stack with default utility -->
  <li>Item 4</li>
</ul>

I've only implemented this for border and margin utilities but I think this could be a global variant as it can be useful when designing tables.

It looks pretty complicated and creates one more thing to learn.

Isn't it more obvious to use not-first/not-last as in #1441?

Implemented in #1584 👍

Isn't it more obvious to use not-first/not-last as in #1441?

@thanosisalive I definitely considered this but for situations where you are not generating things in a loop, it feels sort of gross:

<nav>
  <a href="#" class="not-first:ml-4">Home</a>
  <a href="#" class="not-first:ml-4">Projects</a>
  <a href="#" class="not-first:ml-4">Team</a>
  <a href="#" class="not-first:ml-4">Settings</a>
</nav>

I agree it's simpler from a technical point of view and closer to how the rest of Tailwind has been designed up until this point, but this just feels so much nicer to me that I think it's worth it:

<nav class="space-x-4">
  <a href="#">Home</a>
  <a href="#">Projects</a>
  <a href="#">Team</a>
  <a href="#">Settings</a>
</nav>

🚀 thanks @adamwathan! Looking forward to using the new tools.

It looks pretty complicated and creates one more thing to learn.

Isn't it more obvious to use not-first/not-last as in #1441?

When we talk about space between elements, applying those in a component basis makes no sense. Margins have a side effect, they affect components around them. When applying margins in components we make them less reusable. Margins work better applied in context, from parent to children. That's why the grid formatting context has gap and it's applied in the parent, not children.

Let's work with a real example. A list of social links with icons. They can be set horizontally or vertically —vertically in this example.

/* All elements but first */
.stack\:mt-4 > * + * {
  margin-top: 1rem;
}

Per component basis style

<ul class="pl-0 mb-0">
  <li><a href="#">Facebook</a></li>
  <li class="mt-4"><a href="#">Twitter</a></li>
  <li class="mt-4"><a href="#">Youtube</a></li>
  <li class="mt-4"><a href="#">Instagram</a></li>
</ul>

With parent context

<ul class="pl-0 mb-0 stack:mt-4">
  <li><a href="#">Facebook</a></li>
  <li><a href="#">Twitter</a></li>
  <li><a href="#">Youtube</a></li>
  <li><a href="#">Instagram</a></li>
  <!-- add as many as you like without touching your classes -->
</ul>

In the second example you can add more items to the list without modifying your code. As the “lobotomized owl” technique applies margins to all elements but first —you can do it the other way around, all elements but last.

I encourage devs using frameworks to learn more about the inner workings of CSS. CSS is what makes websites look great. Frameworks help with development but you still need to learn CSS if you want to be more efficient when using this declarative language.

When we talk about space between elements, applying those in a component basis makes no sense. Margins have a side effect, they affect components around them. When applying margins in components we make them less reusable. Margins work better applied in context, from parent to children. That's why the grid formatting context has gap and it's applied in the parent, not children.

I agree, positioning and spacing should not be applied to the components. I usually solve this problem with a component wrapper that's responsible for layout.

As in the list example, the <ul> itself is a layout, <li>'s are layout items, and their contents can be any component and these components don't know anything about their whereabouts.

<ul>
  <li class="not-first:mt-4"><!-- Component --></li>
  <li class="not-first:mt-4"><!-- Component --></li>
  <li class="not-first:mt-4"><!-- Component --></li>
  <li class="not-first:mt-4"><!-- Component --></li>
</ul>

If you don't use wrappers, then at least something like

<div class="children:not-first:mt-4">
  <!-- Component -->
  <!-- Component -->
  <!-- Component -->
  <!-- Component -->
</div>

would be not an ideal solution, but an obvious one.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lamberttraccard picture lamberttraccard  Â·  3Comments

Quineone picture Quineone  Â·  3Comments

spyric picture spyric  Â·  3Comments

jbardnz picture jbardnz  Â·  3Comments

manniL picture manniL  Â·  3Comments