Cluster

Cluster layout primitive for row-style content with wrapping support. Set wrap="nowrap" to keep a small group, such as a meter and its value, on one row. Set maxRows with an overflow slot to show complete rows and reveal the remaining items through your action. The optional trailing slot stays visible within the row limit. The overflow-change event reports the hidden count. Omit maxRows for ordinary unbounded wrapping.

Default

With no properties supplied, Cluster lays tags out in a row with the default gap and wraps them onto new lines when the row runs out of space.

Design Accessibility Performance Docs Testing Release

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-badge/ui-badge.html"
>

<div
  style="max-width: 18rem"
>
  <ui-cluster>
    <ui-badge>Design</ui-badge>
    <ui-badge>Accessibility</ui-badge>
    <ui-badge>Performance</ui-badge>
    <ui-badge>Docs</ui-badge>
    <ui-badge>Testing</ui-badge>
    <ui-badge>Release</ui-badge>
  </ui-cluster>
</div>
<script setup>
  import { UiBadge, UiCluster } from "@threadlabs/looma";
</script>

<template>
  <div
    style="max-width: 18rem"
  >
    <UiCluster>
      <UiBadge>Design</UiBadge>
      <UiBadge>Accessibility</UiBadge>
      <UiBadge>Performance</UiBadge>
      <UiBadge>Docs</UiBadge>
      <UiBadge>Testing</UiBadge>
      <UiBadge>Release</UiBadge>
    </UiCluster>
  </div>
</template>
import { UiBadge, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxWidth":"18rem"}}
  >
    <UiCluster>
      <UiBadge>Design</UiBadge>
      <UiBadge>Accessibility</UiBadge>
      <UiBadge>Performance</UiBadge>
      <UiBadge>Docs</UiBadge>
      <UiBadge>Testing</UiBadge>
      <UiBadge>Release</UiBadge>
    </UiCluster>
  </div>;
}
/** @jsxImportSource preact */
import { UiBadge, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxWidth":"18rem"}}
  >
    <UiCluster>
      <UiBadge>Design</UiBadge>
      <UiBadge>Accessibility</UiBadge>
      <UiBadge>Performance</UiBadge>
      <UiBadge>Docs</UiBadge>
      <UiBadge>Testing</UiBadge>
      <UiBadge>Release</UiBadge>
    </UiCluster>
  </div>;
}
<script>
  import { UiBadge, UiCluster } from "@threadlabs/looma";
</script>

<div
  style="max-width: 18rem"
>
  <UiCluster>
    <UiBadge>Design</UiBadge>
    <UiBadge>Accessibility</UiBadge>
    <UiBadge>Performance</UiBadge>
    <UiBadge>Docs</UiBadge>
    <UiBadge>Testing</UiBadge>
    <UiBadge>Release</UiBadge>
  </UiCluster>
</div>
import { Component } from "@angular/core";
import { UiBadge, UiCluster } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiBadge],
  template: `
    <div
      style="max-width: 18rem"
    >
      <div
        ui-cluster
      >
        <span
          ui-badge
        >Design</span>
        <span
          ui-badge
        >Accessibility</span>
        <span
          ui-badge
        >Performance</span>
        <span
          ui-badge
        >Docs</span>
        <span
          ui-badge
        >Testing</span>
        <span
          ui-badge
        >Release</span>
      </div>
    </div>
  `,
})
export default class Example {}

gap="l"

The large gap token adds more space between tags, both along each row and between wrapped rows.

Design Accessibility Performance Docs Testing Release

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-badge/ui-badge.html"
>

<div
  style="max-width: 18rem"
>
  <ui-cluster
    gap="l"
  >
    <ui-badge>Design</ui-badge>
    <ui-badge>Accessibility</ui-badge>
    <ui-badge>Performance</ui-badge>
    <ui-badge>Docs</ui-badge>
    <ui-badge>Testing</ui-badge>
    <ui-badge>Release</ui-badge>
  </ui-cluster>
</div>
<script setup>
  import { UiBadge, UiCluster } from "@threadlabs/looma";
</script>

<template>
  <div
    style="max-width: 18rem"
  >
    <UiCluster
      gap="l"
    >
      <UiBadge>Design</UiBadge>
      <UiBadge>Accessibility</UiBadge>
      <UiBadge>Performance</UiBadge>
      <UiBadge>Docs</UiBadge>
      <UiBadge>Testing</UiBadge>
      <UiBadge>Release</UiBadge>
    </UiCluster>
  </div>
</template>
import { UiBadge, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxWidth":"18rem"}}
  >
    <UiCluster
      gap="l"
    >
      <UiBadge>Design</UiBadge>
      <UiBadge>Accessibility</UiBadge>
      <UiBadge>Performance</UiBadge>
      <UiBadge>Docs</UiBadge>
      <UiBadge>Testing</UiBadge>
      <UiBadge>Release</UiBadge>
    </UiCluster>
  </div>;
}
/** @jsxImportSource preact */
import { UiBadge, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxWidth":"18rem"}}
  >
    <UiCluster
      gap="l"
    >
      <UiBadge>Design</UiBadge>
      <UiBadge>Accessibility</UiBadge>
      <UiBadge>Performance</UiBadge>
      <UiBadge>Docs</UiBadge>
      <UiBadge>Testing</UiBadge>
      <UiBadge>Release</UiBadge>
    </UiCluster>
  </div>;
}
<script>
  import { UiBadge, UiCluster } from "@threadlabs/looma";
</script>

<div
  style="max-width: 18rem"
>
  <UiCluster
    gap="l"
  >
    <UiBadge>Design</UiBadge>
    <UiBadge>Accessibility</UiBadge>
    <UiBadge>Performance</UiBadge>
    <UiBadge>Docs</UiBadge>
    <UiBadge>Testing</UiBadge>
    <UiBadge>Release</UiBadge>
  </UiCluster>
</div>
import { Component } from "@angular/core";
import { UiBadge, UiCluster } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiBadge],
  template: `
    <div
      style="max-width: 18rem"
    >
      <div
        ui-cluster
        [gap]="'l'"
      >
        <span
          ui-badge
        >Design</span>
        <span
          ui-badge
        >Accessibility</span>
        <span
          ui-badge
        >Performance</span>
        <span
          ui-badge
        >Docs</span>
        <span
          ui-badge
        >Testing</span>
        <span
          ui-badge
        >Release</span>
      </div>
    </div>
  `,
})
export default class Example {}

align="end"

End alignment makes items of different heights share a bottom edge within each row.

Design Docs

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-badge/ui-badge.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>

<ui-cluster
  gap="s"
  align="end"
>
  <ui-badge>Design</ui-badge>
  <ui-badge>Docs</ui-badge>
  <ui-button
    size="sm"
  >Add tag</ui-button>
</ui-cluster>
<script setup>
  import { UiBadge, UiButton, UiCluster } from "@threadlabs/looma";
</script>

<template>
  <UiCluster
    gap="s"
    align="end"
  >
    <UiBadge>Design</UiBadge>
    <UiBadge>Docs</UiBadge>
    <UiButton
      size="sm"
    >Add tag</UiButton>
  </UiCluster>
</template>
import { UiBadge, UiButton, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="s"
    align="end"
  >
    <UiBadge>Design</UiBadge>
    <UiBadge>Docs</UiBadge>
    <UiButton
      size="sm"
    >Add tag</UiButton>
  </UiCluster>;
}
/** @jsxImportSource preact */
import { UiBadge, UiButton, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="s"
    align="end"
  >
    <UiBadge>Design</UiBadge>
    <UiBadge>Docs</UiBadge>
    <UiButton
      size="sm"
    >Add tag</UiButton>
  </UiCluster>;
}
<script>
  import { UiBadge, UiButton, UiCluster } from "@threadlabs/looma";
</script>

<UiCluster
  gap="s"
  align="end"
>
  <UiBadge>Design</UiBadge>
  <UiBadge>Docs</UiBadge>
  <UiButton
    size="sm"
  >Add tag</UiButton>
</UiCluster>
import { Component } from "@angular/core";
import { UiBadge, UiButton, UiCluster } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiBadge, UiButton],
  template: `
    <div
      ui-cluster
      [gap]="'s'"
      [align]="'end'"
    >
      <span
        ui-badge
      >Design</span>
      <span
        ui-badge
      >Docs</span>
      <ng-template
        ui-button
        [size]="'sm'"
      >Add tag</ng-template>
    </div>
  `,
})
export default class Example {}

justify="between"

Justify places items along each row; between pushes the first and last items to the ends, as for a heading with its actions.

Members

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>

<ui-cluster
  justify="between"
>
  <ui-text
    weight="semibold"
  >Members</ui-text>
  <ui-button
    size="sm"
  >Invite</ui-button>
</ui-cluster>
<script setup>
  import { UiButton, UiCluster, UiText } from "@threadlabs/looma";
</script>

<template>
  <UiCluster
    justify="between"
  >
    <UiText
      weight="semibold"
    >Members</UiText>
    <UiButton
      size="sm"
    >Invite</UiButton>
  </UiCluster>
</template>
import { UiButton, UiCluster, UiText } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    justify="between"
  >
    <UiText
      weight="semibold"
    >Members</UiText>
    <UiButton
      size="sm"
    >Invite</UiButton>
  </UiCluster>;
}
/** @jsxImportSource preact */
import { UiButton, UiCluster, UiText } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    justify="between"
  >
    <UiText
      weight="semibold"
    >Members</UiText>
    <UiButton
      size="sm"
    >Invite</UiButton>
  </UiCluster>;
}
<script>
  import { UiButton, UiCluster, UiText } from "@threadlabs/looma";
</script>

<UiCluster
  justify="between"
>
  <UiText
    weight="semibold"
  >Members</UiText>
  <UiButton
    size="sm"
  >Invite</UiButton>
</UiCluster>
import { Component } from "@angular/core";
import { UiButton, UiCluster, UiText } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiText, UiButton],
  template: `
    <div
      ui-cluster
      [justify]="'between'"
    >
      <ng-template
        ui-text
        [weight]="'semibold'"
      >Members</ng-template>
      <ng-template
        ui-button
        [size]="'sm'"
      >Invite</ng-template>
    </div>
  `,
})
export default class Example {}

wrap="nowrap"

Nowrap keeps a small group on one row, so a meter stays beside its amount even in a narrow space.

$750.00

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-meter/ui-meter.html"
>

<div
  style="max-width: 6rem"
>
  <ui-cluster
    gap="s"
    wrap="nowrap"
  > <ui-meter
      size="sm"
      value="750"
      max="1240"
      tone="info"
      label="Collected"
      value-text="$750 of $1,240"
    ></ui-meter> $750.00 </ui-cluster>
</div>
<script setup>
  import { UiCluster, UiMeter } from "@threadlabs/looma";
</script>

<template>
  <div
    style="max-width: 6rem"
  >
    <UiCluster
      gap="s"
      wrap="nowrap"
    > <UiMeter
        size="sm"
        :value="750"
        :max="1240"
        tone="info"
        label="Collected"
        value-text="$750 of $1,240"
      /> $750.00 </UiCluster>
  </div>
</template>
import { UiCluster, UiMeter } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxWidth":"6rem"}}
  >
    <UiCluster
      gap="s"
      wrap="nowrap"
    >{" "}<UiMeter
        size="sm"
        value={750}
        max={1240}
        tone="info"
        label="Collected"
        valueText="$750 of $1,240"
      />{" $750.00 "}</UiCluster>
  </div>;
}
/** @jsxImportSource preact */
import { UiCluster, UiMeter } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxWidth":"6rem"}}
  >
    <UiCluster
      gap="s"
      wrap="nowrap"
    >{" "}<UiMeter
        size="sm"
        value={750}
        max={1240}
        tone="info"
        label="Collected"
        valueText="$750 of $1,240"
      />{" $750.00 "}</UiCluster>
  </div>;
}
<script>
  import { UiCluster, UiMeter } from "@threadlabs/looma";
</script>

<div
  style="max-width: 6rem"
>
  <UiCluster
    gap="s"
    wrap="nowrap"
  > <UiMeter
      size="sm"
      value={750}
      max={1240}
      tone="info"
      label="Collected"
      valueText="$750 of $1,240"
    /> $750.00 </UiCluster>
</div>
import { Component } from "@angular/core";
import { UiCluster, UiMeter } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiMeter],
  template: `
    <div
      style="max-width: 6rem"
    >
      <div
        ui-cluster
        [gap]="'s'"
        [wrap]="'nowrap'"
      > <span
          ui-meter
          [size]="'sm'"
          [value]="750"
          [max]="1240"
          [tone]="'info'"
          [label]="'Collected'"
          [valueText]="'$750 of $1,240'"
        ></span> $750.00 </div>
    </div>
  `,
})
export default class Example {}

Bounded rows with all items available

Reserve an overflow action and an always-visible trailing action within three rows. The popup contains the complete collection.

Research and discovery Interface design Content strategy Accessibility review Implementation Testing and delivery
Research and discovery Interface design Content strategy Accessibility review Implementation Testing and delivery

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-container/ui-container.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-badge/ui-badge.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-popover/ui-popover.html"
>

<ui-container
  fill
>
  <ui-stack>
    <ui-cluster
      max-rows="3"
      gap="xs"
    >
      <ui-badge>Research and discovery</ui-badge>
      <ui-badge>Interface design</ui-badge>
      <ui-badge>Content strategy</ui-badge>
      <ui-badge>Accessibility review</ui-badge>
      <ui-badge>Implementation</ui-badge>
      <ui-badge>Testing and delivery</ui-badge>
      <ui-button
        slot="overflow"
        id="bounded-all-items"
        size="sm"
        variant="ghost"
      >All items…</ui-button>
      <ui-button
        slot="trailing"
        size="sm"
        variant="ghost"
      >Add item</ui-button>
    </ui-cluster>
    <ui-popover
      for="bounded-all-items"
      role="region"
      aria-label="All items"
    >
      <ui-cluster
        gap="xs"
      >
        <ui-badge>Research and discovery</ui-badge>
        <ui-badge>Interface design</ui-badge>
        <ui-badge>Content strategy</ui-badge>
        <ui-badge>Accessibility review</ui-badge>
        <ui-badge>Implementation</ui-badge>
        <ui-badge>Testing and delivery</ui-badge>
      </ui-cluster>
    </ui-popover>
  </ui-stack>
</ui-container>
<script setup>
  import { UiBadge, UiButton, UiCluster, UiContainer, UiPopover, UiStack } from "@threadlabs/looma";
</script>

<template>
  <UiContainer
    :fill="true"
  >
    <UiStack>
      <UiCluster
        :max-rows="3"
        gap="xs"
      >
        <UiBadge>Research and discovery</UiBadge>
        <UiBadge>Interface design</UiBadge>
        <UiBadge>Content strategy</UiBadge>
        <UiBadge>Accessibility review</UiBadge>
        <UiBadge>Implementation</UiBadge>
        <UiBadge>Testing and delivery</UiBadge>
        <template #overflow>
          <UiButton
            id="bounded-all-items"
            size="sm"
            variant="ghost"
          >All items…</UiButton>
        </template>
        <template #trailing>
          <UiButton
            size="sm"
            variant="ghost"
          >Add item</UiButton>
        </template>
      </UiCluster>
      <UiPopover
        for="bounded-all-items"
        role="region"
        aria-label="All items"
      >
        <UiCluster
          gap="xs"
        >
          <UiBadge>Research and discovery</UiBadge>
          <UiBadge>Interface design</UiBadge>
          <UiBadge>Content strategy</UiBadge>
          <UiBadge>Accessibility review</UiBadge>
          <UiBadge>Implementation</UiBadge>
          <UiBadge>Testing and delivery</UiBadge>
        </UiCluster>
      </UiPopover>
    </UiStack>
  </UiContainer>
</template>
import { UiBadge, UiButton, UiCluster, UiContainer, UiPopover, UiStack } from "@threadlabs/looma";

export default function Example() {
  return <UiContainer
    fill
  >
    <UiStack>
      <UiCluster
        maxRows={3}
        gap="xs"
        slots={{
          overflow: <UiButton
            id="bounded-all-items"
            size="sm"
            variant="ghost"
          >All items…</UiButton>,
          trailing: <UiButton
            size="sm"
            variant="ghost"
          >Add item</UiButton>,
        }}
      >
        <UiBadge>Research and discovery</UiBadge>
        <UiBadge>Interface design</UiBadge>
        <UiBadge>Content strategy</UiBadge>
        <UiBadge>Accessibility review</UiBadge>
        <UiBadge>Implementation</UiBadge>
        <UiBadge>Testing and delivery</UiBadge>
      </UiCluster>
      <UiPopover
        htmlFor="bounded-all-items"
        role="region"
        aria-label="All items"
      >
        <UiCluster
          gap="xs"
        >
          <UiBadge>Research and discovery</UiBadge>
          <UiBadge>Interface design</UiBadge>
          <UiBadge>Content strategy</UiBadge>
          <UiBadge>Accessibility review</UiBadge>
          <UiBadge>Implementation</UiBadge>
          <UiBadge>Testing and delivery</UiBadge>
        </UiCluster>
      </UiPopover>
    </UiStack>
  </UiContainer>;
}
/** @jsxImportSource preact */
import { UiBadge, UiButton, UiCluster, UiContainer, UiPopover, UiStack } from "@threadlabs/looma";

export default function Example() {
  return <UiContainer
    fill
  >
    <UiStack>
      <UiCluster
        maxRows={3}
        gap="xs"
        slots={{
          overflow: <UiButton
            id="bounded-all-items"
            size="sm"
            variant="ghost"
          >All items…</UiButton>,
          trailing: <UiButton
            size="sm"
            variant="ghost"
          >Add item</UiButton>,
        }}
      >
        <UiBadge>Research and discovery</UiBadge>
        <UiBadge>Interface design</UiBadge>
        <UiBadge>Content strategy</UiBadge>
        <UiBadge>Accessibility review</UiBadge>
        <UiBadge>Implementation</UiBadge>
        <UiBadge>Testing and delivery</UiBadge>
      </UiCluster>
      <UiPopover
        htmlFor="bounded-all-items"
        role="region"
        aria-label="All items"
      >
        <UiCluster
          gap="xs"
        >
          <UiBadge>Research and discovery</UiBadge>
          <UiBadge>Interface design</UiBadge>
          <UiBadge>Content strategy</UiBadge>
          <UiBadge>Accessibility review</UiBadge>
          <UiBadge>Implementation</UiBadge>
          <UiBadge>Testing and delivery</UiBadge>
        </UiCluster>
      </UiPopover>
    </UiStack>
  </UiContainer>;
}
<script>
  import { UiBadge, UiButton, UiCluster, UiContainer, UiPopover, UiStack } from "@threadlabs/looma";
</script>

{#snippet overflow()}
  <UiButton
    id="bounded-all-items"
    size="sm"
    variant="ghost"
  >All items…</UiButton>
{/snippet}

{#snippet trailing()}
  <UiButton
    size="sm"
    variant="ghost"
  >Add item</UiButton>
{/snippet}

<UiContainer
  fill
>
  <UiStack>
    <UiCluster
      maxRows={3}
      gap="xs"
      slots={{ overflow, trailing }}
    >
      <UiBadge>Research and discovery</UiBadge>
      <UiBadge>Interface design</UiBadge>
      <UiBadge>Content strategy</UiBadge>
      <UiBadge>Accessibility review</UiBadge>
      <UiBadge>Implementation</UiBadge>
      <UiBadge>Testing and delivery</UiBadge>
    </UiCluster>
    <UiPopover
      for="bounded-all-items"
      role="region"
      aria-label="All items"
    >
      <UiCluster
        gap="xs"
      >
        <UiBadge>Research and discovery</UiBadge>
        <UiBadge>Interface design</UiBadge>
        <UiBadge>Content strategy</UiBadge>
        <UiBadge>Accessibility review</UiBadge>
        <UiBadge>Implementation</UiBadge>
        <UiBadge>Testing and delivery</UiBadge>
      </UiCluster>
    </UiPopover>
  </UiStack>
</UiContainer>
import { Component } from "@angular/core";
import { UiBadge, UiButton, UiCluster, UiContainer, UiPopover, UiStack } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiContainer, UiStack, UiCluster, UiBadge, UiButton, UiPopover],
  template: `
    <div
      ui-container
      [fill]="true"
    >
      <div
        ui-stack
      >
        <div
          ui-cluster
          [maxRows]="3"
          [gap]="'xs'"
        >
          <span
            ui-badge
          >Research and discovery</span>
          <span
            ui-badge
          >Interface design</span>
          <span
            ui-badge
          >Content strategy</span>
          <span
            ui-badge
          >Accessibility review</span>
          <span
            ui-badge
          >Implementation</span>
          <span
            ui-badge
          >Testing and delivery</span>
          <ng-container ngProjectAs="[slot='overflow']">
            <ng-template
              ui-button
              [size]="'sm'"
              [variant]="'ghost'"
              [hnAttributes]="{ id: 'bounded-all-items' }"
            >All items…</ng-template>
          </ng-container>
          <ng-container ngProjectAs="[slot='trailing']">
            <ng-template
              ui-button
              [size]="'sm'"
              [variant]="'ghost'"
            >Add item</ng-template>
          </ng-container>
        </div>
        <span
          ui-popover
          [for]="'bounded-all-items'"
          role="region"
          aria-label="All items"
        >
          <div
            ui-cluster
            [gap]="'xs'"
          >
            <span
              ui-badge
            >Research and discovery</span>
            <span
              ui-badge
            >Interface design</span>
            <span
              ui-badge
            >Content strategy</span>
            <span
              ui-badge
            >Accessibility review</span>
            <span
              ui-badge
            >Implementation</span>
            <span
              ui-badge
            >Testing and delivery</span>
          </div>
        </span>
      </div>
    </div>
  `,
})
export default class Example {}

Theme sampler

A compact collection of real controls and a surface for judging a shared theme together.

Fellowship office

Ready

Second breakfast, sorted.

Frodo plans. Sam follows through.

Briefing at nine · Bring the maps.

Draft

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-card/ui-card.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-text/ui-text.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-badge/ui-badge.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon-button/ui-icon-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-input-group/ui-input-group.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-input/ui-input.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-switch/ui-switch.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-radio/ui-radio.html"
>

<ui-card
  treatment="expressive"
>
  <ui-stack
    gap="l"
  >
    <ui-cluster
      gap="xs"
      justify="between"
    >
      <ui-text
        variant="eyebrow"
      >Fellowship office</ui-text>
      <ui-badge
        tone="success"
      >Ready</ui-badge>
    </ui-cluster>
    <ui-stack
      gap="s"
    >
      <ui-text
        variant="heading"
        as="h3"
      >Second breakfast, sorted.</ui-text>
      <ui-text>Frodo plans. Sam follows through.</ui-text>
      <ui-text
        variant="caption"
      >Briefing at nine · Bring the maps.</ui-text>
    </ui-stack>
    <ui-cluster>
      <ui-button
        variant="solid"
        tone="accent"
        title="Solid accent"
      >Send</ui-button>
      <ui-button
        variant="outline"
        tone="accent"
        title="Outline accent"
      >Save</ui-button>
      <ui-button
        variant="solid"
        tone="neutral"
        title="Solid neutral"
      >File</ui-button>
      <ui-button
        variant="outline"
        tone="neutral"
        title="Outline neutral"
      >Archive</ui-button>
      <ui-icon-button
        variant="outline"
        tone="neutral"
        match-button
        label="Acknowledge memo"
      >
        <ui-icon
          name="circle-check"
        ></ui-icon>
      </ui-icon-button>
      <ui-button
        variant="ghost"
        tone="neutral"
        title="Ghost neutral"
      >Later</ui-button>
    </ui-cluster>
    <ui-input-group>
      <ui-icon
        slot="prefix"
        name="search"
      ></ui-icon>
      <ui-input
        aria-label="Search office"
        placeholder="Search Fellowship office"
      ></ui-input>
    </ui-input-group>
    <ui-cluster>
      <ui-checkbox
        checked
      >Notify Sam</ui-checkbox>
      <ui-switch
        checked
      >Available</ui-switch>
      <ui-radio
        checked
      >Daily briefing</ui-radio>
      <ui-badge
        tone="accent"
      >Draft</ui-badge>
    </ui-cluster>
  </ui-stack>
</ui-card>
<script setup>
  import {
    UiBadge,
    UiButton,
    UiCard,
    UiCheckbox,
    UiCluster,
    UiIcon,
    UiIconButton,
    UiInput,
    UiInputGroup,
    UiRadio,
    UiStack,
    UiSwitch,
    UiText,
  } from "@threadlabs/looma";
</script>

<template>
  <UiCard
    treatment="expressive"
  >
    <UiStack
      gap="l"
    >
      <UiCluster
        gap="xs"
        justify="between"
      >
        <UiText
          variant="eyebrow"
        >Fellowship office</UiText>
        <UiBadge
          tone="success"
        >Ready</UiBadge>
      </UiCluster>
      <UiStack
        gap="s"
      >
        <UiText
          variant="heading"
          as="h3"
        >Second breakfast, sorted.</UiText>
        <UiText>Frodo plans. Sam follows through.</UiText>
        <UiText
          variant="caption"
        >Briefing at nine · Bring the maps.</UiText>
      </UiStack>
      <UiCluster>
        <UiButton
          variant="solid"
          tone="accent"
          title="Solid accent"
        >Send</UiButton>
        <UiButton
          variant="outline"
          tone="accent"
          title="Outline accent"
        >Save</UiButton>
        <UiButton
          variant="solid"
          tone="neutral"
          title="Solid neutral"
        >File</UiButton>
        <UiButton
          variant="outline"
          tone="neutral"
          title="Outline neutral"
        >Archive</UiButton>
        <UiIconButton
          variant="outline"
          tone="neutral"
          :match-button="true"
          label="Acknowledge memo"
        >
          <UiIcon
            name="circle-check"
          />
        </UiIconButton>
        <UiButton
          variant="ghost"
          tone="neutral"
          title="Ghost neutral"
        >Later</UiButton>
      </UiCluster>
      <UiInputGroup>
        <template #prefix>
          <UiIcon
            name="search"
          />
        </template>
        <UiInput
          aria-label="Search office"
          placeholder="Search Fellowship office"
        />
      </UiInputGroup>
      <UiCluster>
        <UiCheckbox
          :checked="true"
        >Notify Sam</UiCheckbox>
        <UiSwitch
          :checked="true"
        >Available</UiSwitch>
        <UiRadio
          :checked="true"
        >Daily briefing</UiRadio>
        <UiBadge
          tone="accent"
        >Draft</UiBadge>
      </UiCluster>
    </UiStack>
  </UiCard>
</template>
import {
  UiBadge,
  UiButton,
  UiCard,
  UiCheckbox,
  UiCluster,
  UiIcon,
  UiIconButton,
  UiInput,
  UiInputGroup,
  UiRadio,
  UiStack,
  UiSwitch,
  UiText,
} from "@threadlabs/looma";

export default function Example() {
  return <UiCard
    treatment="expressive"
  >
    <UiStack
      gap="l"
    >
      <UiCluster
        gap="xs"
        justify="between"
      >
        <UiText
          variant="eyebrow"
        >Fellowship office</UiText>
        <UiBadge
          tone="success"
        >Ready</UiBadge>
      </UiCluster>
      <UiStack
        gap="s"
      >
        <UiText
          variant="heading"
          as="h3"
        >Second breakfast, sorted.</UiText>
        <UiText>Frodo plans. Sam follows through.</UiText>
        <UiText
          variant="caption"
        >Briefing at nine · Bring the maps.</UiText>
      </UiStack>
      <UiCluster>
        <UiButton
          variant="solid"
          tone="accent"
          title="Solid accent"
        >Send</UiButton>
        <UiButton
          variant="outline"
          tone="accent"
          title="Outline accent"
        >Save</UiButton>
        <UiButton
          variant="solid"
          tone="neutral"
          title="Solid neutral"
        >File</UiButton>
        <UiButton
          variant="outline"
          tone="neutral"
          title="Outline neutral"
        >Archive</UiButton>
        <UiIconButton
          variant="outline"
          tone="neutral"
          matchButton
          label="Acknowledge memo"
        >
          <UiIcon
            name="circle-check"
          />
        </UiIconButton>
        <UiButton
          variant="ghost"
          tone="neutral"
          title="Ghost neutral"
        >Later</UiButton>
      </UiCluster>
      <UiInputGroup
        slots={{
          prefix: <UiIcon
            name="search"
          />,
        }}
      >
        <UiInput
          aria-label="Search office"
          placeholder="Search Fellowship office"
        />
      </UiInputGroup>
      <UiCluster>
        <UiCheckbox
          checked
        >Notify Sam</UiCheckbox>
        <UiSwitch
          checked
        >Available</UiSwitch>
        <UiRadio
          checked
        >Daily briefing</UiRadio>
        <UiBadge
          tone="accent"
        >Draft</UiBadge>
      </UiCluster>
    </UiStack>
  </UiCard>;
}
/** @jsxImportSource preact */
import {
  UiBadge,
  UiButton,
  UiCard,
  UiCheckbox,
  UiCluster,
  UiIcon,
  UiIconButton,
  UiInput,
  UiInputGroup,
  UiRadio,
  UiStack,
  UiSwitch,
  UiText,
} from "@threadlabs/looma";

export default function Example() {
  return <UiCard
    treatment="expressive"
  >
    <UiStack
      gap="l"
    >
      <UiCluster
        gap="xs"
        justify="between"
      >
        <UiText
          variant="eyebrow"
        >Fellowship office</UiText>
        <UiBadge
          tone="success"
        >Ready</UiBadge>
      </UiCluster>
      <UiStack
        gap="s"
      >
        <UiText
          variant="heading"
          as="h3"
        >Second breakfast, sorted.</UiText>
        <UiText>Frodo plans. Sam follows through.</UiText>
        <UiText
          variant="caption"
        >Briefing at nine · Bring the maps.</UiText>
      </UiStack>
      <UiCluster>
        <UiButton
          variant="solid"
          tone="accent"
          title="Solid accent"
        >Send</UiButton>
        <UiButton
          variant="outline"
          tone="accent"
          title="Outline accent"
        >Save</UiButton>
        <UiButton
          variant="solid"
          tone="neutral"
          title="Solid neutral"
        >File</UiButton>
        <UiButton
          variant="outline"
          tone="neutral"
          title="Outline neutral"
        >Archive</UiButton>
        <UiIconButton
          variant="outline"
          tone="neutral"
          matchButton
          label="Acknowledge memo"
        >
          <UiIcon
            name="circle-check"
          />
        </UiIconButton>
        <UiButton
          variant="ghost"
          tone="neutral"
          title="Ghost neutral"
        >Later</UiButton>
      </UiCluster>
      <UiInputGroup
        slots={{
          prefix: <UiIcon
            name="search"
          />,
        }}
      >
        <UiInput
          aria-label="Search office"
          placeholder="Search Fellowship office"
        />
      </UiInputGroup>
      <UiCluster>
        <UiCheckbox
          checked
        >Notify Sam</UiCheckbox>
        <UiSwitch
          checked
        >Available</UiSwitch>
        <UiRadio
          checked
        >Daily briefing</UiRadio>
        <UiBadge
          tone="accent"
        >Draft</UiBadge>
      </UiCluster>
    </UiStack>
  </UiCard>;
}
<script>
  import {
    UiBadge,
    UiButton,
    UiCard,
    UiCheckbox,
    UiCluster,
    UiIcon,
    UiIconButton,
    UiInput,
    UiInputGroup,
    UiRadio,
    UiStack,
    UiSwitch,
    UiText,
  } from "@threadlabs/looma";
</script>

{#snippet prefix()}
  <UiIcon
    name="search"
  />
{/snippet}

<UiCard
  treatment="expressive"
>
  <UiStack
    gap="l"
  >
    <UiCluster
      gap="xs"
      justify="between"
    >
      <UiText
        variant="eyebrow"
      >Fellowship office</UiText>
      <UiBadge
        tone="success"
      >Ready</UiBadge>
    </UiCluster>
    <UiStack
      gap="s"
    >
      <UiText
        variant="heading"
        as="h3"
      >Second breakfast, sorted.</UiText>
      <UiText>Frodo plans. Sam follows through.</UiText>
      <UiText
        variant="caption"
      >Briefing at nine · Bring the maps.</UiText>
    </UiStack>
    <UiCluster>
      <UiButton
        variant="solid"
        tone="accent"
        title="Solid accent"
      >Send</UiButton>
      <UiButton
        variant="outline"
        tone="accent"
        title="Outline accent"
      >Save</UiButton>
      <UiButton
        variant="solid"
        tone="neutral"
        title="Solid neutral"
      >File</UiButton>
      <UiButton
        variant="outline"
        tone="neutral"
        title="Outline neutral"
      >Archive</UiButton>
      <UiIconButton
        variant="outline"
        tone="neutral"
        matchButton
        label="Acknowledge memo"
      >
        <UiIcon
          name="circle-check"
        />
      </UiIconButton>
      <UiButton
        variant="ghost"
        tone="neutral"
        title="Ghost neutral"
      >Later</UiButton>
    </UiCluster>
    <UiInputGroup
      slots={{ prefix }}
    >
      <UiInput
        aria-label="Search office"
        placeholder="Search Fellowship office"
      />
    </UiInputGroup>
    <UiCluster>
      <UiCheckbox
        checked
      >Notify Sam</UiCheckbox>
      <UiSwitch
        checked
      >Available</UiSwitch>
      <UiRadio
        checked
      >Daily briefing</UiRadio>
      <UiBadge
        tone="accent"
      >Draft</UiBadge>
    </UiCluster>
  </UiStack>
</UiCard>
import { Component } from "@angular/core";
import {
  UiBadge,
  UiButton,
  UiCard,
  UiCheckbox,
  UiCluster,
  UiIcon,
  UiIconButton,
  UiInput,
  UiInputGroup,
  UiRadio,
  UiStack,
  UiSwitch,
  UiText,
} from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCard, UiStack, UiCluster, UiText, UiBadge, UiButton, UiIconButton, UiIcon, UiInputGroup, UiInput, UiCheckbox, UiSwitch, UiRadio],
  template: `
    <ng-template
      ui-card
      [treatment]="'expressive'"
    >
      <div
        ui-stack
        [gap]="'l'"
      >
        <div
          ui-cluster
          [gap]="'xs'"
          [justify]="'between'"
        >
          <ng-template
            ui-text
            [variant]="'eyebrow'"
          >Fellowship office</ng-template>
          <span
            ui-badge
            [tone]="'success'"
          >Ready</span>
        </div>
        <div
          ui-stack
          [gap]="'s'"
        >
          <ng-template
            ui-text
            [variant]="'heading'"
            [as]="'h3'"
          >Second breakfast, sorted.</ng-template>
          <ng-template
            ui-text
          >Frodo plans. Sam follows through.</ng-template>
          <ng-template
            ui-text
            [variant]="'caption'"
          >Briefing at nine · Bring the maps.</ng-template>
        </div>
        <div
          ui-cluster
        >
          <ng-template
            ui-button
            [variant]="'solid'"
            [tone]="'accent'"
            [hnAttributes]="{ title: 'Solid accent' }"
          >Send</ng-template>
          <ng-template
            ui-button
            [variant]="'outline'"
            [tone]="'accent'"
            [hnAttributes]="{ title: 'Outline accent' }"
          >Save</ng-template>
          <ng-template
            ui-button
            [variant]="'solid'"
            [tone]="'neutral'"
            [hnAttributes]="{ title: 'Solid neutral' }"
          >File</ng-template>
          <ng-template
            ui-button
            [variant]="'outline'"
            [tone]="'neutral'"
            [hnAttributes]="{ title: 'Outline neutral' }"
          >Archive</ng-template>
          <button
            ui-icon-button
            [variant]="'outline'"
            [tone]="'neutral'"
            [matchButton]="true"
            [label]="'Acknowledge memo'"
          >
            <span
              ui-icon
              name="circle-check"
            ></span>
          </button>
          <ng-template
            ui-button
            [variant]="'ghost'"
            [tone]="'neutral'"
            [hnAttributes]="{ title: 'Ghost neutral' }"
          >Later</ng-template>
        </div>
        <div
          ui-input-group
        >
          <ng-container ngProjectAs="[slot='prefix']">
            <span
              ui-icon
              name="search"
            ></span>
          </ng-container>
          <input
            ui-input
            aria-label="Search office"
            placeholder="Search Fellowship office"
          >
        </div>
        <div
          ui-cluster
        >
          <label
            ui-checkbox
            [checked]="true"
          >Notify Sam</label>
          <label
            ui-switch
            [checked]="true"
          >Available</label>
          <label
            ui-radio
            [checked]="true"
          >Daily briefing</label>
          <span
            ui-badge
            [tone]="'accent'"
          >Draft</span>
        </div>
      </div>
    </ng-template>
  `,
})
export default class Example {}

API

Package: @threadlabs/looma · Native root: div

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
alignHow items line up within each row: center (the default) aligns their middles, start their tops, end their bottoms, and stretch makes every item as tall as its row. baseline aligns the first text baselines, including multiline columns.alignkeyword—start, center, end, stretch, baseline
fixedKeeps this group at its content size inside a flex layout.fixedbooleanfalse—
gapSpace between items, on the spacing scale: xs is 0.5rem, s 0.75rem (also the default), m 1rem, l 1.5rem, and xl 2rem. The --ui-cluster-gap custom property, set on :root, an ancestor, or the component itself, overrides it.gapkeyword—xs, s, m, l, xl
justifyWhere items sit along each row: start (the default), center, end, or between, which pushes the first and last items to the ends of the row and spreads the spare space between them, as for a title with its actions.justifykeyword—start, center, end, between
max-rowsMaximum complete visible rows when wrapping. Zero leaves the group unbounded. Supply an overflow slot with a control that opens the complete collection; extra direct items are hidden and leave keyboard navigation. The trailing slot stays visible and counts toward the row cap.maxRowsinteger0—
paddingOptional internal space for an action group, using the shared spacing scale. xs is 0.25rem, s is 0.5rem, m is 0.75rem, and l is 1rem.paddingkeywordnonenone, xs, s, m, l
padding-endOptional extra space at the inline end, such as clearance beside another control. xl is 4rem.paddingEndkeywordnonenone, s, m, l, xl
wrapWhether items move onto a new row when they run out of space: wrap (the default) or nowrap, which keeps them on one row at their own sizes. Use nowrap for a small group that must read as one unit, such as a meter and its value in a table cell, so the cell grows to fit it; a whole row of actions should keep wrapping so it fits narrow screens.wrapkeyword—wrap, nowrap

Framework props

NameTypeDefaultAllowed values
align"start" | "center" | "end" | "stretch" | "baseline"—start, center, end, stretch, baseline
fixedbooleanfalse—
gap"xs" | "s" | "m" | "l" | "xl"—xs, s, m, l, xl
justify"start" | "center" | "end" | "between"—start, center, end, between
maxRowsnumber0—
padding"none" | "xs" | "s" | "m" | "l"nonenone, xs, s, m, l
paddingEnd"none" | "s" | "m" | "l" | "xl"nonenone, s, m, l, xl
wrap"wrap" | "nowrap"—wrap, nowrap

Events

NameDetailDescription
overflow-changeobject
  • count: integer (required)
Emitted with the declared detail payload.

Slots

NamePurpose
defaultDefault child content.
overflowNamed content for the overflow region.
trailingNamed content for the trailing region.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-cluster-gapvar(--ui-space-3) | var(--ui-space-2) | var(--ui-space-4) | var(--ui-space-5) | var(--ui-space-6)—

Shared tokens

TokenFallback
--ui-space-1—
--ui-space-2—
--ui-space-3—
--ui-space-4—
--ui-space-5—
--ui-space-6—