Blocks · Application

Application 6

In-product surfaces: settings, members, empty states, utilities, and support chat.

Settings panel

Notification switches, workspace form, and a plan row.

CardSwitchInputTagButton

Notifications

Workspace

PlanPro
---
import { Card, Switch, Input, Tag, Button } from "astro-ui";
---

<div class="block-canvas-grid">
  <Card>
    <h3 slot="header">Notifications</h3>
    <div class="block-settings">
      <Switch name="product-updates" checked label="Product updates" />
      <Switch name="weekly-digest" label="Weekly digest" />
      <Switch name="security-alerts" checked label="Security alerts" />
    </div>
  </Card>
  <Card>
    <h3 slot="header">Workspace</h3>
    <div class="block-form">
      <div class="block-field">
        <label for="workspace">Workspace name</label>
        <Input name="workspace" id="workspace" value="Acme Inc" />
      </div>
      <div class="block-form-row">
        <span class="block-plan-label">Plan</span>
        <Tag variant="primary">Pro</Tag>
      </div>
      <Button variant="outline">Save changes</Button>
    </div>
  </Card>
</div>

<style is:global>
  .block-canvas-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--ui-space-4); }
  .block-settings { display: grid; gap: var(--ui-space-3); }
  .block-form { display: grid; gap: var(--ui-space-3); }
  .block-field { display: grid; gap: var(--ui-space-1); }
  .block-field label { font-size: var(--ui-text-sm); font-weight: var(--ui-font-medium); }
  .block-form-row { display: flex; align-items: center; justify-content: space-between; gap: var(--ui-space-3); }
  .block-plan-label { font-size: var(--ui-text-sm); color: var(--ui-muted-foreground); }
</style>

Team members

Member list with avatars, role badges, and invite action.

CardAvatarBadgeButton

Members

  • Parsa MownerOwner
  • Devin VmaintainerMaintainer
  • Robin HmemberContributor
---
import { Card, Avatar, Badge, Button } from "astro-ui";
---

<Card>
  <h3 slot="header">Members</h3>
  <ul class="block-team">
    {[
      { name: "Parsa M", role: "Owner", badge: "owner" },
      { name: "Devin V", role: "Maintainer", badge: "maintainer" },
      { name: "Robin H", role: "Contributor", badge: "member" },
    ].map((member) => (
      <li class="block-team-row">
        <Avatar name={member.name} size="sm" />
        <span class="block-team-name">{member.name}</span>
        <Badge
          variant={member.badge === "owner" ? "primary" : member.badge === "maintainer" ? "info" : "muted"}
          size="sm"
        >
          {member.badge}
        </Badge>
        <span class="block-team-role">{member.role}</span>
      </li>
    ))}
  </ul>
  <Button slot="footer" variant="outline" size="sm">Invite member</Button>
</Card>

<style is:global>
  .block-team { display: grid; gap: var(--ui-space-2); margin: 0; padding: 0; list-style: none; }
  .block-team-row { display: flex; align-items: center; gap: var(--ui-space-3); padding: var(--ui-space-2) 0; }
  .block-team-name { font-size: var(--ui-text-sm); font-weight: var(--ui-font-medium); }
  .block-team-role { margin-inline-start: auto; font-size: var(--ui-text-xs); color: var(--ui-muted-foreground); }
</style>

Empty state

Zero-data panel with icon, copy, and next-step actions.

EmptyButton

No projects yet
Create your first project to see analytics, deploys, and team activity here.
---
import { Empty, Button } from "astro-ui";
---

<Empty>
  <span slot="media" class="block-empty-icon" aria-hidden="true">◈</span>
  <span slot="title">No projects yet</span>
  <span slot="description">
    Create your first project to see analytics, deploys, and team activity here.
  </span>
  <Button size="sm">New project</Button>
  <Button size="sm" variant="ghost" href="/docs/get-started/">Read the guide</Button>
</Empty>

<style is:global>
  .block-empty-icon { display: grid; place-items: center; font-size: 1.75rem; color: var(--ui-primary); }
</style>

Empty state — with avatar

Empty state that leads with a single avatar and a next-step CTA.

EmptyAvatarButton

No team members yet
Invite your first teammate to start collaborating on projects, deploys, and analytics.
---
import { Empty, Avatar, Button } from "astro-ui";
---

<Empty size="lg" ariaLabel="No team members yet">
  <span slot="media" class="block-empty-avatar" aria-hidden="true">
    <Avatar name="Your team" size="xl" variant="primary" ring="primary" />
  </span>
  <span slot="title">No team members yet</span>
  <span slot="description">Invite your first teammate to start collaborating on projects, deploys, and analytics.</span>
  <Button size="sm">Invite member</Button>
  <Button size="sm" variant="ghost" href="/docs/get-started/">Read the guide</Button>
</Empty>

<style is:global>
  .block-empty-avatar { display: grid; place-items: center; }
  /* Empty handles layout; Avatar sits in the media slot for a human-centered empty state. */
</style>

Password generator

Generate a strong password: length slider, character-set switches, and a one-click generate action.

CardSliderSwitchButton

Generate Password

CLICK GENERATE
Length 16
Settings
---
import { Card, Slider, Switch, Button } from "astro-ui";
---

<div class="passgen">
  <Card class="passgen__card">
    <h3 slot="header" class="passgen__title">Generate Password</h3>

    <output class="passgen__out" data-passgen-output aria-live="polite">CLICK GENERATE</output>

    <div class="passgen__field">
      <span class="passgen__lab">Length <b data-passgen-length>16</b></span>
      <Slider name="passgen-length" id="passgen-length" min={4} max={32} value={16}
              ariaStartLabel="Password length" />
      <div class="passgen__bounds" aria-hidden="true">
        <span>4</span><span>32</span>
      </div>
    </div>

    <div class="passgen__field">
      <span class="passgen__lab">Settings</span>
      <div class="passgen__opts">
        <Switch name="passgen-upper" checked label="Include Uppercase" />
        <Switch name="passgen-lower" checked label="Include Lowercase" />
        <Switch name="passgen-num" checked label="Include Numbers" />
        <Switch name="passgen-sym" label="Include Symbols" />
      </div>
    </div>

    <Button variant="primary" data-passgen-generate class="passgen__btn">
      Generate Password
    </Button>
  </Card>
</div>

<script>
  // Wire every generator on the page: length readout + generate click.
  document.querySelectorAll("[data-passgen-generate]").forEach((btn) => {
    const root = btn.closest(".passgen__card");
    if (!root) return;
    const output = root.querySelector("[data-passgen-output]");
    const lenOut = root.querySelector("[data-passgen-length]");
    const range = root.querySelector("input[type='range']");
    const boxes = Array.from(root.querySelectorAll("input[type='checkbox']"));
    const SETS = [
      "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      "abcdefghijklmnopqrstuvwxyz",
      "0123456789",
      "!@#$%^&*()_+-=[]{};:,.<>?/",
    ];
    btn.addEventListener("click", () => {
      if (!output || !range) return;
      const len = Number(range.value) || 16;
      const pool = boxes.map((b, i) => (b.checked ? SETS[i] : "")).join("");
      if (!pool) {
        output.textContent = "SELECT AT LEAST ONE SET";
        return;
      }
      const bytes = crypto.getRandomValues(new Uint32Array(len));
      let pass = "";
      for (let i = 0; i < len; i++) pass += pool[bytes[i] % pool.length];
      output.textContent = pass;
    });
    range.addEventListener("input", () => {
      if (lenOut) lenOut.textContent = range.value;
    });
  });
</script>

<style is:global>
  .passgen { display: grid; justify-items: center; }
  .passgen__card { inline-size: min(24rem, 100%); }
  .passgen__title { margin: 0; color: var(--ui-foreground); font-size: var(--ui-text-lg); font-weight: var(--ui-font-semibold); }
  .passgen__out { display: block; padding: var(--ui-space-3) var(--ui-space-4); margin-block-end: var(--ui-space-4); background-color: var(--ui-muted); border-radius: var(--ui-radius-md); color: var(--ui-foreground); font-size: var(--ui-text-md); letter-spacing: 0.08em; text-align: center; overflow-wrap: anywhere; }
  .passgen__field { display: grid; gap: var(--ui-space-2); margin-block-end: var(--ui-space-4); }
  .passgen__lab { color: var(--ui-muted-foreground); font-size: var(--ui-text-xs); font-weight: var(--ui-font-semibold); letter-spacing: 0.08em; text-transform: uppercase; }
  .passgen__lab b { color: var(--ui-foreground); }
  .passgen__bounds { display: flex; justify-content: space-between; color: var(--ui-muted-foreground); font-size: var(--ui-text-xs); }
  .passgen__opts { display: grid; gap: var(--ui-space-2); }
  .passgen__btn { inline-size: 100%; margin-block-start: var(--ui-space-2); }
</style>

Support chat thread

Operator + customer conversation: grouped bubbles, reactions, quick-reply buttons, and a composer.

BubbleBubbleGroupAvatarButtonInput

Mira · SupportTypically replies in minutes
Online

Today · 09:12

👋 Hi! I'm Mira from support. I see ticket #4821 opened on your account — the double charge on last month's invoice, right?
I've already pulled up the billing log.

You · 09:14

Yes! My card was charged twice for the renewal 🙁
(and the renewal email never arrived either)

Mira · 09:15

Confirmed — duplicate transaction at 14:02. Refund approved, lands in 3–5 business days.

You · 09:16

Amazing, thank you! Will I get a confirmation email?

Mira · 09:17

You will — and here's the refund reference for your records:
RFND-2026-09482

You · 09:18

Mira · 09:19

All set on my side! I'll stay in this thread if anything else comes up. Quick actions before you go:
Ticket #4821 marked as resolved · Mira · 09:21
---
import {
  Bubble,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
  Avatar,
  Badge,
  Button,
  Input,
} from "astro-ui";
---

<div class="chat">
  <div class="chat__head">
    <Avatar name="Mira Chen" size="sm" status="online" />
    <div class="chat__who">
      <span class="chat__name">Mira · Support</span>
      <span class="chat__sub">Typically replies in minutes</span>
    </div>
    <Badge variant="success" size="sm">Online</Badge>
  </div>

  <div class="chat__thread">
    <p class="chat__meta">Today · 09:12</p>

    <!-- Operator turn: consecutive messages collapse into one group -->
    <div class="chat__turn">
      <Avatar name="Mira Chen" size="sm" />
      <BubbleGroup ariaLabel="Messages from Mira" class="chat__group">
        <Bubble align="start" variant="muted">
          <BubbleContent>👋 Hi! I'm Mira from support. I see ticket #4821 opened on your account — the double charge on last month's invoice, right?</BubbleContent>
        </Bubble>
        <Bubble align="start" variant="muted">
          <BubbleContent>I've already pulled up the billing log.</BubbleContent>
        </Bubble>
      </BubbleGroup>
    </div>

    <p class="chat__meta chat__meta--end">You · 09:14</p>
    <div class="chat__turn chat__turn--end">
      <BubbleGroup ariaLabel="Messages from you" class="chat__group">
        <Bubble align="end" variant="primary">
          <BubbleContent>Yes! My card was charged twice for the renewal 🙁</BubbleContent>
        </Bubble>
        <Bubble align="end" variant="primary">
          <BubbleContent>(and the renewal email never arrived either)</BubbleContent>
          <BubbleReactions side="bottom" align="end" ariaLabel="Reactions">
            <button type="button" aria-pressed="true" aria-label="Heart, 1 reaction">
              <span aria-hidden="true">❤️</span> 1
            </button>
          </BubbleReactions>
        </Bubble>
      </BubbleGroup>
      <Avatar name="Sam Visitor" size="sm" />
    </div>

    <!-- Keep alternating turns; add row-gap so reaction pills have room -->
    <p class="chat__meta">Mira · 09:15</p>
    <div class="chat__turn">
      <Avatar name="Mira Chen" size="sm" />
      <Bubble align="start" variant="muted">
        <BubbleContent>Confirmed — duplicate transaction at 14:02. Refund approved, lands in 3–5 business days.</BubbleContent>
      </Bubble>
    </div>

    <p class="chat__meta chat__meta--end">You · 09:16</p>
    <div class="chat__turn chat__turn--end">
      <Bubble align="end" variant="primary">
        <BubbleContent>Amazing, thank you! Will I get a confirmation email?</BubbleContent>
        <BubbleReactions side="bottom" align="start" ariaLabel="Reactions">
          <span aria-hidden="true">🙏 2</span>
        </BubbleReactions>
      </Bubble>
    </div>

    <p class="chat__meta">Mira · 09:17</p>
    <div class="chat__turn">
      <Avatar name="Mira Chen" size="sm" />
      <BubbleGroup ariaLabel="Messages from Mira" class="chat__group">
        <Bubble align="start" variant="muted">
          <BubbleContent>You will — and here's the refund reference for your records:</BubbleContent>
        </Bubble>
        <Bubble align="start" variant="outline">
          <BubbleContent><code>RFND-2026-09482</code></BubbleContent>
        </Bubble>
      </BubbleGroup>
    </div>

    <!-- Interactive bubble: a real button the user can pick -->
    <p class="chat__meta chat__meta--end">You · 09:18</p>
    <div class="chat__turn chat__turn--end">
      <Bubble align="end" variant="tinted">
        <button type="button" class="aui-bubble__content">Perfect — anything else I should know?</button>
      </Bubble>
    </div>

    <p class="chat__meta">Mira · 09:19</p>
    <div class="chat__turn">
      <Avatar name="Mira Chen" size="sm" />
      <Bubble align="start" variant="muted">
        <BubbleContent>All set on my side! I'll stay in this thread if anything else comes up. Quick actions before you go:</BubbleContent>
      </Bubble>
    </div>

    <!-- Suggested user actions -->
    <div class="chat__quick">
      <Button variant="outline" size="sm" shape="rounded">Email me the transcript</Button>
      <Button variant="outline" size="sm" shape="rounded">Rate this chat</Button>
      <Button variant="primary" size="sm" shape="rounded">Open new ticket</Button>
    </div>

    <!-- Ghost system event closes the thread -->
    <div class="chat__resolved">
      <Bubble align="start" variant="ghost">
        <BubbleContent>Ticket #4821 marked as <strong>resolved</strong> · Mira · 09:21</BubbleContent>
      </Bubble>
    </div>
  </div>

  <div class="chat__composer">
    <Input name="chat-message" placeholder="Type and press enter…" ariaLabel="Message" />
    <Button variant="primary" size="icon" shape="rounded" ariaLabel="Send message">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>
    </Button>
  </div>
</div>

<style is:global>
  .chat { display: grid; gap: var(--ui-space-3); max-inline-size: 34rem; }
  .chat__head { display: flex; align-items: center; gap: var(--ui-space-2); padding-bottom: var(--ui-space-2); border-bottom: 1px solid var(--ui-border); }
  .chat__who { display: grid; gap: 2px; margin-inline-end: auto; }
  .chat__name { font-size: var(--ui-text-sm); font-weight: var(--ui-font-semibold); color: var(--ui-foreground); }
  .chat__sub { font-size: var(--ui-text-xs); color: var(--ui-muted-foreground); }
  .chat__thread { display: flex; flex-direction: column; gap: 1.75rem; max-block-size: 30rem; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
  .chat__meta { margin: 0; font-size: var(--ui-text-xs); color: var(--ui-muted-foreground); }
  .chat__meta--end { text-align: end; }
  .chat__turn { display: flex; align-items: flex-end; gap: var(--ui-space-2); }
  .chat__turn--end { flex-direction: row-reverse; }
  .chat__group { gap: var(--ui-space-1); }
  .chat__thread :is(.aui-bubble__content p) { margin: 0; }
  .chat__thread :is(.aui-bubble__content code) { padding: 0.05em 0.35em; background: color-mix(in srgb, currentcolor 10%, transparent); border-radius: var(--ui-radius-sm); font-family: var(--ui-font-mono); font-size: 0.85em; }
  .chat__quick { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }
  .chat__resolved { padding-inline-start: var(--ui-space-2); }
  .chat__composer { display: flex; align-items: center; gap: var(--ui-space-2); padding-top: var(--ui-space-2); border-top: 1px solid var(--ui-border); }
  .chat__composer :is(input, .aui-input) { flex: 1; }
</style>