Layout

Stack

Lays children out in a column or a row with a token gap, alignment, wrapping and optional dividers.

Built on
Flexboxgap
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.4 kB
Dependencies
React
Release checklist2 of 3
Tag v2.4.0
Publish release notes
Notify #platform
Try its options

Loading its options.

Code
import { Stack } from "@/components/equip/stack";

const block = {
  padding: "6px 10px",
  borderRadius: "var(--eq-radius-s)",
  background: "var(--eq-tint-strong)",
  fontSize: "var(--eq-text-xs)",
  color: "var(--eq-ink-2)",
};

export default function Demo() {
  return (
    <Stack gap={2} style={{ width: 208 }}>
      <Stack direction="row" gap={2} justify="between">
        <strong style={{ fontSize: "var(--eq-text-s)" }}>Release checklist</strong>
        <span style={{ fontSize: "var(--eq-text-xs)", color: "var(--eq-ink-2)" }}>2 of 3</span>
      </Stack>
      <Stack gap={2} divider>
        <div style={block}>Tag v2.4.0</div>
        <div style={block}>Publish release notes</div>
        <div style={block}>Notify #platform</div>
      </Stack>
    </Stack>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:208px" class="eq-stack" data-direction="column" data-gap="2">
  <div class="eq-stack" data-direction="row" data-gap="2" data-justify="between">
    <strong style="font-size:var(--eq-text-s)">Release checklist</strong>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">2 of 3</span>
  </div>
  <div class="eq-stack" data-direction="column" data-gap="2">
    <div style="padding:6px 10px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Tag v2.4.0</div>
    <div class="eq-stack-divider" aria-hidden="true"></div>
    <div style="padding:6px 10px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Publish release notes</div>
    <div class="eq-stack-divider" aria-hidden="true"></div>
    <div style="padding:6px 10px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong);font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Notify #platform</div>
  </div>
</div>

Examples

A list with dividers

With as="ul" the hairlines are li elements hidden from assistive tech, so the list still counts three items.

  • Two-factor authenticationOn, using an authenticator app
  • Recovery codes8 of 10 left
  • Active sessions3 devices, last seen in Lisbon
Code
import { Button } from "@/components/equip/button";
import { Stack } from "@/components/equip/stack";

const rows = [
  { name: "Two-factor authentication", detail: "On, using an authenticator app", action: "Manage" },
  { name: "Recovery codes", detail: "8 of 10 left", action: "Regenerate" },
  { name: "Active sessions", detail: "3 devices, last seen in Lisbon", action: "Review" },
];

export default function Example() {
  return (
    <Stack as="ul" gap={4} divider style={{ width: "100%", maxWidth: 520 }}>
      {rows.map((row) => (
        <li key={row.name}>
          <Stack direction="row" gap={3} justify="between" wrap>
            <Stack gap={1} style={{ minWidth: 0 }}>
              <strong style={{ fontSize: "var(--eq-text-m)" }}>{row.name}</strong>
              <span style={{ fontSize: "var(--eq-text-s)", color: "var(--eq-ink-2)" }}>{row.detail}</span>
            </Stack>
            <Button size="s" variant="outline">
              {row.action}
            </Button>
          </Stack>
        </li>
      ))}
    </Stack>
  );
}
HTMLworks in any framework, with the part's CSS
<ul style="width:100%;max-width:520px" class="eq-stack" data-direction="column" data-gap="4">
  <li>
    <div class="eq-stack" data-direction="row" data-gap="3" data-justify="between" data-wrap="true">
      <div style="min-width:0" class="eq-stack" data-direction="column" data-gap="1">
        <strong style="font-size:var(--eq-text-m)">Two-factor authentication</strong>
        <span style="font-size:var(--eq-text-s);color:var(--eq-ink-2)">On, using an authenticator app</span>
      </div>
      <button type="button" class="eq-button" data-variant="outline" data-size="s">
        <span class="eq-button-label">Manage</span>
      </button>
    </div>
  </li>
  <li class="eq-stack-divider" aria-hidden="true"></li>
  <li>
    <div class="eq-stack" data-direction="row" data-gap="3" data-justify="between" data-wrap="true">
      <div style="min-width:0" class="eq-stack" data-direction="column" data-gap="1">
        <strong style="font-size:var(--eq-text-m)">Recovery codes</strong>
        <span style="font-size:var(--eq-text-s);color:var(--eq-ink-2)">8 of 10 left</span>
      </div>
      <button type="button" class="eq-button" data-variant="outline" data-size="s">
        <span class="eq-button-label">Regenerate</span>
      </button>
    </div>
  </li>
  <li class="eq-stack-divider" aria-hidden="true"></li>
  <li>
    <div class="eq-stack" data-direction="row" data-gap="3" data-justify="between" data-wrap="true">
      <div style="min-width:0" class="eq-stack" data-direction="column" data-gap="1">
        <strong style="font-size:var(--eq-text-m)">Active sessions</strong>
        <span style="font-size:var(--eq-text-s);color:var(--eq-ink-2)">3 devices, last seen in Lisbon</span>
      </div>
      <button type="button" class="eq-button" data-variant="outline" data-size="s">
        <span class="eq-button-label">Review</span>
      </button>
    </div>
  </li>
</ul>

A wrapping row

wrap lets the actions drop under the title on a narrow screen instead of squeezing it.

Invoices142 this quarter
Code
import { Button } from "@/components/equip/button";
import { Stack } from "@/components/equip/stack";

export default function Example() {
  return (
    <Stack
      as="header"
      direction="row"
      gap={3}
      justify="between"
      wrap
      style={{ width: "100%", maxWidth: 560 }}
    >
      <Stack gap={1}>
        <strong style={{ fontSize: "var(--eq-text-l)" }}>Invoices</strong>
        <span style={{ fontSize: "var(--eq-text-s)", color: "var(--eq-ink-2)" }}>142 this quarter</span>
      </Stack>
      <Stack direction="row" gap={2} divider>
        <Button size="s" variant="ghost">
          Export CSV
        </Button>
        <Button size="s" variant="accent">
          New invoice
        </Button>
      </Stack>
    </Stack>
  );
}
HTMLworks in any framework, with the part's CSS
<header style="width:100%;max-width:560px" class="eq-stack" data-direction="row" data-gap="3" data-justify="between" data-wrap="true">
  <div class="eq-stack" data-direction="column" data-gap="1">
    <strong style="font-size:var(--eq-text-l)">Invoices</strong>
    <span style="font-size:var(--eq-text-s);color:var(--eq-ink-2)">142 this quarter</span>
  </div>
  <div class="eq-stack" data-direction="row" data-gap="2">
    <button type="button" class="eq-button" data-variant="ghost" data-size="s">
      <span class="eq-button-label">Export CSV</span>
    </button>
    <div class="eq-stack-divider" aria-hidden="true"></div>
    <button type="button" class="eq-button" data-variant="accent" data-size="s">
      <span class="eq-button-label">New invoice</span>
    </button>
  </div>
</header>

Gap steps

gap 1 to 6 reads --eq-space-1 to --eq-space-6, so spacing follows the finish.

gap 1
gap 2
gap 3
gap 4
gap 5
gap 6
Code
import { Stack } from "@/components/equip/stack";

const steps = [1, 2, 3, 4, 5, 6] as const;

export default function Example() {
  return (
    <Stack direction="row" gap={5} wrap align="start">
      {steps.map((step) => (
        <Stack key={step} gap={step} align="center">
          <span style={{ width: 40, height: 14, borderRadius: "var(--eq-radius-s)", background: "var(--eq-tint-strong)" }} />
          <span style={{ width: 40, height: 14, borderRadius: "var(--eq-radius-s)", background: "var(--eq-tint-strong)" }} />
          <span style={{ fontSize: "var(--eq-text-xs)", color: "var(--eq-ink-2)" }}>gap {step}</span>
        </Stack>
      ))}
    </Stack>
  );
}
HTMLworks in any framework, with the part's CSS
<div class="eq-stack" data-direction="row" data-gap="5" data-align="start" data-wrap="true">
  <div class="eq-stack" data-direction="column" data-gap="1" data-align="center">
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">gap 1</span>
  </div>
  <div class="eq-stack" data-direction="column" data-gap="2" data-align="center">
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">gap 2</span>
  </div>
  <div class="eq-stack" data-direction="column" data-gap="3" data-align="center">
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">gap 3</span>
  </div>
  <div class="eq-stack" data-direction="column" data-gap="4" data-align="center">
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">gap 4</span>
  </div>
  <div class="eq-stack" data-direction="column" data-gap="5" data-align="center">
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">gap 5</span>
  </div>
  <div class="eq-stack" data-direction="column" data-gap="6" data-align="center">
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="width:40px;height:14px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></span>
    <span style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">gap 6</span>
  </div>
</div>

Props

PropTypeDefaultWhat it does
as"div" | "section" | "ul" | "ol" | "nav" | "header" | "footer" | "form" | "article" | "aside""div"The root element. Use ul or ol when the children are li.
direction"column" | "row""column"column flows down, row flows across.
gap0 | 1 | 2 | 3 | 4 | 5 | 63Space between children, read from --eq-space-1 to --eq-space-6. 0 is none.
align"start" | "center" | "end" | "stretch" | "baseline"noneCross-axis alignment. A column stretches by default, a row centres.
justify"start" | "center" | "end" | "between"noneMain-axis distribution.
wrapbooleanfalseLets a row wrap onto more lines when it runs out of room.
dividerbooleanfalseDraws a hairline between neighbouring children, centred in the gap.
...restComponentProps<"div">noneEvery native attribute of the root element.

Anatomy

root.eq-stackThe flex container; direction, gap, align and justify are data attributes.
divider.eq-stack-dividerAn aria-hidden div, or li when the root is a list, placed between children.

Notes

  • Renders on the server with no client JavaScript.
  • Visual order always equals DOM order, so the reading and tab order match what people see.
  • Dividers are decorative and hidden from assistive tech; use Divider between sections that need a separator role.
  • With as="ul" or "ol" the dividers are li elements, so the list stays valid HTML and its item count is unchanged for screen readers.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/stack.tsx65 lines
import { Children, Fragment, type ComponentProps } from "react";
import "./stack.css";

type Space = 0 | 1 | 2 | 3 | 4 | 5 | 6;

export type StackProps = ComponentProps<"div"> & {
  /** The element at the root. Use ul or ol when the children are li. */
  as?: "div" | "section" | "ul" | "ol" | "nav" | "header" | "footer" | "form" | "article" | "aside";
  /** column flows down, row flows across. */
  direction?: "column" | "row";
  /** Space between children, from --eq-space-1 to --eq-space-6. 0 is none. */
  gap?: Space;
  /** Cross-axis alignment of the children. */
  align?: "start" | "center" | "end" | "stretch" | "baseline";
  /** Main-axis distribution of the children. */
  justify?: "start" | "center" | "end" | "between";
  /** Lets a row wrap onto more lines when it runs out of room. */
  wrap?: boolean;
  /** Draws a hairline between neighbouring children, in the middle of the gap. */
  divider?: boolean;
};

export function Stack({
  as = "div",
  direction = "column",
  gap = 3,
  align,
  justify,
  wrap = false,
  divider = false,
  className,
  children,
  ...rest
}: StackProps) {
  // The root changes element but never its attributes, so the div typing holds for all of them.
  const Root = as as "div";
  const list = as === "ul" || as === "ol";
  const items = divider ? Children.toArray(children) : null;

  return (
    <Root
      {...rest}
      className={["eq-stack", className].filter(Boolean).join(" ")}
      data-direction={direction}
      data-gap={gap}
      data-align={align}
      data-justify={justify}
      data-wrap={wrap || undefined}
    >
      {items
        ? items.map((child, index) => (
            <Fragment key={index}>
              {index > 0 &&
                (list ? (
                  <li className="eq-stack-divider" aria-hidden="true" />
                ) : (
                  <div className="eq-stack-divider" aria-hidden="true" />
                ))}
              {child}
            </Fragment>
          ))
        : children}
    </Root>
  );
}
components/equip/stack.css62 lines
@layer equip {
  .eq-stack {
    --_gap: var(--eq-space-3);

    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: var(--_gap);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
    list-style: none;
  }

  .eq-stack[data-direction="row"] {
    flex-direction: row;
    align-items: center;
  }

  .eq-stack[data-gap="0"] { --_gap: 0px; }
  .eq-stack[data-gap="1"] { --_gap: var(--eq-space-1); }
  .eq-stack[data-gap="2"] { --_gap: var(--eq-space-2); }
  .eq-stack[data-gap="3"] { --_gap: var(--eq-space-3); }
  .eq-stack[data-gap="4"] { --_gap: var(--eq-space-4); }
  .eq-stack[data-gap="5"] { --_gap: var(--eq-space-5); }
  .eq-stack[data-gap="6"] { --_gap: var(--eq-space-6); }

  .eq-stack[data-align="start"] { align-items: flex-start; }
  .eq-stack[data-align="center"] { align-items: center; }
  .eq-stack[data-align="end"] { align-items: flex-end; }
  .eq-stack[data-align="stretch"] { align-items: stretch; }
  .eq-stack[data-align="baseline"] { align-items: baseline; }

  .eq-stack[data-justify="start"] { justify-content: flex-start; }
  .eq-stack[data-justify="center"] { justify-content: center; }
  .eq-stack[data-justify="end"] { justify-content: flex-end; }
  .eq-stack[data-justify="between"] { justify-content: space-between; }

  .eq-stack[data-wrap] {
    flex-wrap: wrap;
  }

  /* The divider is a flex item, so the gap falls on both sides of it.
     Pulling it in by half a gap each way keeps the total spacing equal to the gap. */
  .eq-stack-divider {
    flex: none;
    align-self: stretch;
    height: 1px;
    margin: calc(var(--_gap) / -2) 0;
    padding: 0;
    background: var(--eq-line);
    list-style: none;
  }

  .eq-stack[data-direction="row"] > .eq-stack-divider {
    width: 1px;
    height: auto;
    min-height: 1em;
    margin: 0 calc(var(--_gap) / -2);
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index