Layout

Section

A titled block with a description and actions, stacked or split beside its content as on a settings page.

Built on
<section>aria-labelledbyContainer queries
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.5 kB
Dependencies
React

API keys

2 active, 1 expiring soon

Try its options

Loading its options.

Code
import { Button } from "@/components/equip/button";
import { Section } from "@/components/equip/section";

const row = {
  height: 22,
  borderRadius: "var(--eq-radius-s)",
  background: "var(--eq-tint-strong)",
};

export default function Demo() {
  return (
    <Section
      title="API keys"
      description="2 active, 1 expiring soon"
      actions={
        <Button size="s" variant="outline">
          New key
        </Button>
      }
      style={{ width: 208 }}
    >
      <div style={row} />
    </Section>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<section style="width:208px" class="eq-section" data-layout="stacked">
  <div class="eq-section-layout">
    <div class="eq-section-header">
      <div class="eq-section-heading">
        <h2 class="eq-section-title">API keys</h2>
        <p class="eq-section-description">2 active, 1 expiring soon</p>
      </div>
      <div class="eq-section-actions">
        <button type="button" class="eq-button" data-variant="outline" data-size="s">
          <span class="eq-button-label">New key</span>
        </button>
      </div>
    </div>
    <div class="eq-section-body">
      <div style="height:22px;border-radius:var(--eq-radius-s);background:var(--eq-tint-strong)"></div>
    </div>
  </div>
</section>

Examples

A settings page

split puts each title beside its fields once the section is 40rem wide and stacks them below that; an id names each section so it becomes a region.

Profile

Shown to people in your workspace.

Delete workspace

Removes every project, member and invoice. This cannot be undone.

Code
import { Button } from "@/components/equip/button";
import { Section } from "@/components/equip/section";

const label = { display: "grid", gap: 4, fontSize: "var(--eq-text-s)", fontWeight: "var(--eq-weight-strong)" } as const;
const input = {
  height: 34,
  padding: "0 12px",
  border: "1px solid var(--eq-line-strong)",
  borderRadius: "var(--eq-radius-m)",
  background: "var(--eq-raised)",
  color: "var(--eq-ink)",
  font: "inherit",
  fontWeight: "var(--eq-weight)",
} as const;

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 24, width: "100%" }}>
      <Section id="settings-profile" layout="split" title="Profile" description="Shown to people in your workspace.">
        <div style={{ display: "grid", gap: 12, maxWidth: 360 }}>
          <label style={label}>
            Display name
            <input style={input} defaultValue="Mira Holt" />
          </label>
          <label style={label}>
            Role
            <input style={input} defaultValue="Finance lead" />
          </label>
        </div>
      </Section>
      <Section
        id="settings-danger"
        layout="split"
        divider
        title="Delete workspace"
        description="Removes every project, member and invoice. This cannot be undone."
      >
        <Button variant="danger">Delete Larder</Button>
      </Section>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:24px;width:100%">
  <section aria-labelledby="settings-profile-title" aria-describedby="settings-profile-description" id="settings-profile" class="eq-section" data-layout="split">
    <div class="eq-section-layout">
      <div class="eq-section-header">
        <div class="eq-section-heading">
          <h2 id="settings-profile-title" class="eq-section-title">Profile</h2>
          <p id="settings-profile-description" class="eq-section-description">Shown to people in your workspace.</p>
        </div>
      </div>
      <div class="eq-section-body">
        <div style="display:grid;gap:12px;max-width:360px">
          <label style="display:grid;gap:4px;font-size:var(--eq-text-s);font-weight:var(--eq-weight-strong)">Display name<input style="height:34px;padding:0 12px;border:1px solid var(--eq-line-strong);border-radius:var(--eq-radius-m);background:var(--eq-raised);color:var(--eq-ink);font:inherit;font-weight:var(--eq-weight)" value="Mira Holt"></label>
          <label style="display:grid;gap:4px;font-size:var(--eq-text-s);font-weight:var(--eq-weight-strong)">Role<input style="height:34px;padding:0 12px;border:1px solid var(--eq-line-strong);border-radius:var(--eq-radius-m);background:var(--eq-raised);color:var(--eq-ink);font:inherit;font-weight:var(--eq-weight)" value="Finance lead"></label>
        </div>
      </div>
    </div>
  </section>
  <section aria-labelledby="settings-danger-title" aria-describedby="settings-danger-description" id="settings-danger" class="eq-section" data-layout="split" data-divider="true">
    <div class="eq-section-layout">
      <div class="eq-section-header">
        <div class="eq-section-heading">
          <h2 id="settings-danger-title" class="eq-section-title">Delete workspace</h2>
          <p id="settings-danger-description" class="eq-section-description">Removes every project, member and invoice. This cannot be undone.</p>
        </div>
      </div>
      <div class="eq-section-body">
        <button type="button" class="eq-button" data-variant="danger" data-size="m">
          <span class="eq-button-label">Delete Larder</span>
        </button>
      </div>
    </div>
  </section>
</div>

A dashboard panel

The actions wrap under the title when the section is narrow, so neither is squeezed.

Traffic

Last 24 hours across all regions

Requests
4.2M
Error rate
0.12%
p95 latency
184 ms
Code
import { Button } from "@/components/equip/button";
import { Section } from "@/components/equip/section";

const stats = [
  ["Requests", "4.2M"],
  ["Error rate", "0.12%"],
  ["p95 latency", "184 ms"],
];

export default function Example() {
  return (
    <Section
      id="traffic"
      level={3}
      title="Traffic"
      description="Last 24 hours across all regions"
      actions={
        <>
          <Button size="s" variant="ghost">
            View logs
          </Button>
          <Button size="s" variant="outline">
            Export
          </Button>
        </>
      }
      style={{ width: "100%", maxWidth: 560 }}
    >
      <dl style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(7rem, 1fr))", gap: 12, margin: 0 }}>
        {stats.map(([term, value]) => (
          <div key={term} style={{ padding: 12, borderRadius: "var(--eq-radius-m)", background: "var(--eq-tint)" }}>
            <dt style={{ fontSize: "var(--eq-text-xs)", color: "var(--eq-ink-2)" }}>{term}</dt>
            <dd style={{ margin: 0, fontSize: "var(--eq-text-l)", fontWeight: "var(--eq-weight-strong)" }}>{value}</dd>
          </div>
        ))}
      </dl>
    </Section>
  );
}
HTMLworks in any framework, with the part's CSS
<section aria-labelledby="traffic-title" aria-describedby="traffic-description" style="width:100%;max-width:560px" id="traffic" class="eq-section" data-layout="stacked">
  <div class="eq-section-layout">
    <div class="eq-section-header">
      <div class="eq-section-heading">
        <h3 id="traffic-title" class="eq-section-title">Traffic</h3>
        <p id="traffic-description" class="eq-section-description">Last 24 hours across all regions</p>
      </div>
      <div class="eq-section-actions">
        <button type="button" class="eq-button" data-variant="ghost" data-size="s">
          <span class="eq-button-label">View logs</span>
        </button>
        <button type="button" class="eq-button" data-variant="outline" data-size="s">
          <span class="eq-button-label">Export</span>
        </button>
      </div>
    </div>
    <div class="eq-section-body">
      <dl style="display:grid;grid-template-columns:repeat(auto-fit, minmax(7rem, 1fr));gap:12px;margin:0">
        <div style="padding:12px;border-radius:var(--eq-radius-m);background:var(--eq-tint)">
          <dt style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Requests</dt>
          <dd style="margin:0;font-size:var(--eq-text-l);font-weight:var(--eq-weight-strong)">4.2M</dd>
        </div>
        <div style="padding:12px;border-radius:var(--eq-radius-m);background:var(--eq-tint)">
          <dt style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">Error rate</dt>
          <dd style="margin:0;font-size:var(--eq-text-l);font-weight:var(--eq-weight-strong)">0.12%</dd>
        </div>
        <div style="padding:12px;border-radius:var(--eq-radius-m);background:var(--eq-tint)">
          <dt style="font-size:var(--eq-text-xs);color:var(--eq-ink-2)">p95 latency</dt>
          <dd style="margin:0;font-size:var(--eq-text-l);font-weight:var(--eq-weight-strong)">184 ms</dd>
        </div>
      </dl>
    </div>
  </div>
</section>

Props

PropTypeDefaultWhat it does
titleReactNodenoneThe heading text.
descriptionReactNodenoneOne or two sentences under the title.
actionsReactNodenoneButtons or links at the end of the header.
level2 | 3 | 42The heading element. Match it to the outline of the page.
layout"stacked" | "split""stacked"split puts the header in a column beside the content once the section is 40rem wide.
dividerbooleanfalseA hairline and space above, to separate it from the section before.
idstringnoneGives the heading the id "<id>-title" and labels the section with it, which makes the section a region landmark.
...restComponentProps<"section">noneEvery native section attribute.

Anatomy

root.eq-sectionThe section and the size container for its layout.
layout.eq-section-layoutThe grid that stacks or splits.
header.eq-section-header
heading.eq-section-headingHolds the title and description.
title.eq-section-title
description.eq-section-description
actions.eq-section-actions
body.eq-section-body

Notes

  • Renders on the server with no client JavaScript.
  • With an id the section is named by its title and described by its description, so it is announced as a region with that name.
  • Without an id it is an unnamed section, which assistive tech does not treat as a landmark; use that for small blocks so the landmark list stays short.
  • The split layout answers to the section's own width, not the viewport, so it also stacks inside a narrow column.

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/section.tsx69 lines
import type { ComponentProps, ReactNode } from "react";
import "./section.css";

export type SectionProps = Omit<ComponentProps<"section">, "title"> & {
  title: ReactNode;
  /** One or two sentences under the title. */
  description?: ReactNode;
  /** Buttons or links at the end of the header. */
  actions?: ReactNode;
  /** The heading element. Match it to the outline of the page. */
  level?: 2 | 3 | 4;
  /**
   * stacked puts the header above the content. split puts it in a column
   * beside the content, as on a settings page, and stacks when narrow.
   */
  layout?: "stacked" | "split";
  /** A hairline above the section, to separate it from the one before. */
  divider?: boolean;
};

/**
 * A titled block of content. Pass an id and the section is named by its
 * title, which makes it a region landmark.
 */
export function Section({
  title,
  description,
  actions,
  level = 2,
  layout = "stacked",
  divider = false,
  id,
  className,
  children,
  ...rest
}: SectionProps) {
  const Heading = `h${level}` as "h2";
  const titleId = id ? `${id}-title` : undefined;
  const descriptionId = id && description != null ? `${id}-description` : undefined;

  return (
    <section
      aria-labelledby={titleId}
      aria-describedby={descriptionId}
      {...rest}
      id={id}
      className={["eq-section", className].filter(Boolean).join(" ")}
      data-layout={layout}
      data-divider={divider || undefined}
    >
      <div className="eq-section-layout">
        <div className="eq-section-header">
          <div className="eq-section-heading">
            <Heading id={titleId} className="eq-section-title">
              {title}
            </Heading>
            {description != null && (
              <p id={descriptionId} className="eq-section-description">
                {description}
              </p>
            )}
          </div>
          {actions != null && <div className="eq-section-actions">{actions}</div>}
        </div>
        <div className="eq-section-body">{children}</div>
      </div>
    </section>
  );
}
components/equip/section.css82 lines
@layer equip {
  .eq-section {
    box-sizing: border-box;
    display: block;
    min-width: 0;
    color: var(--eq-ink);
    font: var(--eq-weight) var(--eq-text-m) / 1.5 var(--eq-font);
    /* The section is the container; the layout inside answers to its width. */
    container: eq-section / inline-size;
  }

  .eq-section[data-divider] {
    padding-top: var(--eq-space-5);
    border-top: 1px solid var(--eq-line);
  }

  .eq-section-layout {
    display: grid;
    gap: var(--eq-space-4);
  }

  .eq-section-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--eq-space-2) var(--eq-space-4);
  }

  .eq-section-heading {
    flex: 1 1 14rem;
    min-width: 0;
  }

  .eq-section-title {
    margin: 0;
    font-size: var(--eq-text-l);
    font-weight: var(--eq-weight-strong);
    line-height: 1.3;
    letter-spacing: -0.01em;
    text-wrap: balance;
  }

  .eq-section-description {
    max-width: 60ch;
    margin: var(--eq-space-1) 0 0;
    color: var(--eq-ink-2);
    font-size: var(--eq-text-s);
    text-wrap: pretty;
  }

  .eq-section-actions {
    flex: none;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--eq-space-2);
  }

  .eq-section-body {
    min-width: 0;
  }

  /* Split: the header becomes a column beside the content once there is room for both. */
  @container eq-section (width >= 40rem) {
    .eq-section[data-layout="split"] > .eq-section-layout {
      grid-template-columns: minmax(12rem, 1fr) minmax(0, 2fr);
      gap: var(--eq-space-6);
    }

    .eq-section[data-layout="split"] > .eq-section-layout > .eq-section-header {
      flex-direction: column;
      flex-wrap: nowrap;
      justify-content: flex-start;
      gap: var(--eq-space-3);
    }

    .eq-section[data-layout="split"] > .eq-section-layout > .eq-section-header > .eq-section-heading {
      flex: none;
    }
  }
}

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