@use 'sprucecss/scss/spruce' as *;
.member-list {
background-color: color('surface');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('lg');
padding: spacer('m');
@include layout-stack('m');
&__header {
@include layout-stack('xxs');
}
&__title {
align-items: center;
display: flex;
font-size: font-size('h5');
gap: spacer('xs');
margin: 0;
}
&__count {
background-color: color('surface-muted');
border: config('border-width-base', $display) solid color('border');
border-radius: radius('pill');
color: color('muted');
font-family: config('font-family-base', $typography);
font-size: 0.75rem;
line-height: 1;
padding: 0.25em 0.6em;
}
&__description {
color: color('muted');
font-size: config('font-size-sm', $typography);
}
&__invite {
align-items: end;
display: flex;
flex-wrap: wrap;
gap: spacer('s');
}
&__invite-email,
&__invite-role {
@include layout-stack('xxs');
}
&__invite-email {
flex: 1 1 14rem;
}
&__invite-role {
flex: 0 1 9rem;
}
&__items {
border-block-start: config('border-width-base', $display) solid color('border');
list-style: none;
padding: 0;
}
&__item {
align-items: center;
border-block-end: config('border-width-base', $display) solid color('border');
display: grid;
gap: spacer('xs') spacer('s');
grid-template-areas:
'person person'
'role actions';
grid-template-columns: minmax(0, 9rem) 1fr;
margin: 0 !important;
padding-block: spacer('s');
@include breakpoint('sm') {
grid-template-areas: 'person role actions';
grid-template-columns: minmax(0, 1fr) 8rem 10rem;
}
&:last-child {
border-block-end: 0;
padding-block-end: 0;
}
}
&__person {
align-items: center;
display: flex;
gap: spacer('s');
grid-area: person;
min-inline-size: 0;
}
&__avatar {
align-items: center;
aspect-ratio: 1;
background-color: color-alpha('primary', 0.15);
border-radius: radius('pill');
color: color('primary');
display: inline-flex;
flex-shrink: 0;
font-size: config('font-size-sm', $typography);
font-weight: 700;
inline-size: 2.5rem;
justify-content: center;
svg {
block-size: 1.125rem;
inline-size: 1.125rem;
}
&--pending {
background-color: transparent;
border: 1px dashed color('border-strong');
color: color('muted');
}
}
&__info {
display: grid;
line-height: config('line-height-md', $typography);
min-inline-size: 0;
}
&__name {
align-items: center;
color: color('heading');
display: flex;
font-weight: 700;
gap: spacer('xs');
@include word-wrap;
}
&__badge {
background-color: color-alpha('primary', 0.12);
border-radius: radius('pill');
color: color('primary');
font-size: 0.6875rem;
padding: 0.15em 0.6em;
}
&__email {
color: color('muted');
font-size: config('font-size-sm', $typography);
@include text-ellipsis;
}
&__role {
grid-area: role;
}
&__actions {
display: flex;
gap: spacer('xxs');
grid-area: actions;
justify-content: flex-end;
}
&__note {
color: color('muted');
font-size: 0.75rem;
hyphens: manual;
text-align: end;
}
&__item--pending &__name {
font-weight: 500;
}
}
<section class="member-list" aria-labelledby="member-list-title" data-member-list>
<header class="member-list__header">
<h3 class="member-list__title" id="member-list-title">Team members <span class="member-list__count" data-member-count>4</span></h3>
<p class="member-list__description">Invite your colleagues and choose what they can access.</p>
</header>
<form class="member-list__invite" action="#" method="post" data-member-invite>
<div class="member-list__invite-email">
<label class="form-label" for="member-invite-email">Email address</label>
<input class="form-control" id="member-invite-email" name="email" type="email" placeholder="name@company.com" autocomplete="off" required>
</div>
<div class="member-list__invite-role">
<label class="form-label" for="member-invite-role">Role</label>
<select class="form-control" id="member-invite-role" name="role">
<option value="admin">Admin</option>
<option value="editor" selected>Editor</option>
<option value="viewer">Viewer</option>
</select>
</div>
<button class="btn btn--primary" type="submit">Send invite</button>
</form>
<ul class="member-list__items" role="list" data-member-items>
<li class="member-list__item" data-member>
<div class="member-list__person">
<span class="member-list__avatar" aria-hidden="true">AS</span>
<span class="member-list__info">
<span class="member-list__name">Anna Smith <span class="member-list__badge">You</span></span>
<span class="member-list__email">anna@acme.com</span>
</span>
</div>
<div class="member-list__role">
<label class="sr-only" for="member-role-anna">Role of Anna Smith</label>
<select class="form-control form-control--sm" id="member-role-anna" name="role" disabled><option value="owner" selected>Owner</option><option value="admin">Admin</option><option value="editor">Editor</option><option value="viewer">Viewer</option></select>
</div>
<div class="member-list__actions">
<span class="member-list__note">Owners can’t remove themselves</span>
</div>
</li>
<li class="member-list__item" data-member>
<div class="member-list__person">
<span class="member-list__avatar" aria-hidden="true">BK</span>
<span class="member-list__info">
<span class="member-list__name">Ben King</span>
<span class="member-list__email">ben@acme.com</span>
</span>
</div>
<div class="member-list__role">
<label class="sr-only" for="member-role-ben">Role of Ben King</label>
<select class="form-control form-control--sm" id="member-role-ben" name="role"><option value="owner">Owner</option><option value="admin" selected>Admin</option><option value="editor">Editor</option><option value="viewer">Viewer</option></select>
</div>
<div class="member-list__actions">
<button class="btn btn--ghost-danger btn--sm" type="button" data-member-remove>Remove<span class="sr-only"> Ben King</span></button>
</div>
</li>
<li class="member-list__item" data-member>
<div class="member-list__person">
<span class="member-list__avatar" aria-hidden="true">LM</span>
<span class="member-list__info">
<span class="member-list__name">Lea Moreau</span>
<span class="member-list__email">lea@acme.com</span>
</span>
</div>
<div class="member-list__role">
<label class="sr-only" for="member-role-lea">Role of Lea Moreau</label>
<select class="form-control form-control--sm" id="member-role-lea" name="role"><option value="owner">Owner</option><option value="admin">Admin</option><option value="editor" selected>Editor</option><option value="viewer">Viewer</option></select>
</div>
<div class="member-list__actions">
<button class="btn btn--ghost-danger btn--sm" type="button" data-member-remove>Remove<span class="sr-only"> Lea Moreau</span></button>
</div>
</li>
<li class="member-list__item member-list__item--pending" data-member>
<div class="member-list__person">
<span class="member-list__avatar member-list__avatar--pending" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 5L2 7"/></svg></span>
<span class="member-list__info">
<span class="member-list__name">jon.diaz@example.com</span>
<span class="member-list__email">Invitation sent <time datetime="2026-10-02">2 days ago</time></span>
</span>
</div>
<div class="member-list__role">
<label class="sr-only" for="member-role-jon-diaz">Role of jon.diaz@example.com</label>
<select class="form-control form-control--sm" id="member-role-jon-diaz" name="role"><option value="owner">Owner</option><option value="admin">Admin</option><option value="editor">Editor</option><option value="viewer" selected>Viewer</option></select>
</div>
<div class="member-list__actions">
<button class="btn btn--ghost-secondary btn--sm" type="button">Resend<span class="sr-only"> the invitation to jon.diaz@example.com</span></button>
<button class="btn btn--ghost-danger btn--sm" type="button" data-member-remove>Revoke<span class="sr-only"> the invitation of jon.diaz@example.com</span></button>
</div>
</li>
</ul>
<template data-member-template>
<li class="member-list__item member-list__item--pending" data-member>
<div class="member-list__person">
<span class="member-list__avatar member-list__avatar--pending" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 5L2 7"/></svg></span>
<span class="member-list__info">
<span class="member-list__name" data-member-email></span>
<span class="member-list__email">Invitation sent just now</span>
</span>
</div>
<div class="member-list__role">
<label class="sr-only" data-member-role-label>Role</label>
<select class="form-control form-control--sm" name="role" data-member-role><option value="admin">Admin</option><option value="editor">Editor</option><option value="viewer">Viewer</option></select>
</div>
<div class="member-list__actions">
<button class="btn btn--ghost-danger btn--sm" type="button" data-member-remove>Revoke<span class="sr-only" data-member-remove-label></span></button>
</div>
</li>
</template>
<p class="sr-only" aria-live="polite" data-member-live></p>
</section>
(() => {
document.querySelectorAll('[data-member-list]').forEach((component) => {
const form = component.querySelector('[data-member-invite]');
const list = component.querySelector('[data-member-items]');
const template = component.querySelector('[data-member-template]');
const count = component.querySelector('[data-member-count]');
const live = component.querySelector('[data-member-live]');
let uid = 0;
const updateCount = () => { count.textContent = list.children.length; };
form.addEventListener('submit', (event) => {
event.preventDefault();
const email = form.elements.email.value.trim();
const role = form.elements.role.value;
const item = template.content.firstElementChild.cloneNode(true);
const select = item.querySelector('[data-member-role]');
const id = `member-role-new-${++uid}`;
item.querySelector('[data-member-email]').textContent = email;
item.querySelector('[data-member-role-label]').textContent = `Role of ${email}`;
item.querySelector('[data-member-role-label]').htmlFor = id;
item.querySelector('[data-member-remove-label]').textContent = ` the invitation of ${email}`;
select.id = id;
select.value = role;
list.append(item);
updateCount();
live.textContent = `Invitation sent to ${email} as ${select.selectedOptions[0].text}.`;
component.dispatchEvent(new CustomEvent('member:invite', { detail: { email, role } }));
form.reset();
form.elements.email.focus();
});
list.addEventListener('click', (event) => {
const button = event.target.closest('[data-member-remove]');
if (!button) {
return;
}
const item = button.closest('[data-member]');
const name = item.querySelector('.member-list__name').firstChild.textContent.trim();
const next = item.nextElementSibling || item.previousElementSibling;
item.remove();
updateCount();
live.textContent = `${name} removed.`;
component.dispatchEvent(new CustomEvent('member:remove', { detail: { name } }));
(next?.querySelector('select:not([disabled]), button') || form.elements.email).focus();
});
});
})();