/* Follow toggles: flip aria-pressed and, when the button names a counter (data-follow="id"), add one follower.
Tabs: click or arrow keys / Home / End move between tabs; inactive panels are hidden only once this runs,
so without JavaScript both panels show one after the other. */
document.querySelectorAll('.cr-btn[aria-pressed]').forEach(function (btn) {
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') !== 'true';
btn.setAttribute('aria-pressed', String(on));
var count = btn.dataset.follow && document.getElementById(btn.dataset.follow);
if (count) count.textContent = (Number(count.dataset.count) + (on ? 1 : 0)).toLocaleString('en-US');
});
});
document.querySelectorAll('.cr [role=tablist]').forEach(function (list) {
var tabs = Array.prototype.slice.call(list.querySelectorAll('[role=tab]'));
function select(tab, focus) {
tabs.forEach(function (t) {
var on = t === tab;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
});
if (focus) tab.focus();
}
list.addEventListener('click', function (e) { var t = e.target.closest('[role=tab]'); if (t) select(t); });
list.addEventListener('keydown', function (e) {
var i = tabs.indexOf(document.activeElement);
var next = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key];
if (i < 0 || next === undefined) return;
e.preventDefault();
select(tabs[(next + tabs.length) % tabs.length], true);
});
select(tabs.filter(function (t) { return t.getAttribute('aria-selected') === 'true'; })[0] || tabs[0]);
});
/* Bootstrap's button plugin flips .active and aria-pressed on Follow, and its tab plugin runs the tabs (arrow keys included).
This listener runs after Bootstrap's and adds one follower to the counter a button names with data-follow="id". */
document.addEventListener('click', function (e) {
var btn = e.target.closest('[data-follow]');
if (!btn) return;
var count = document.getElementById(btn.dataset.follow);
var on = btn.getAttribute('aria-pressed') === 'true';
if (count) count.textContent = (Number(count.dataset.count) + (on ? 1 : 0)).toLocaleString('en-US');
});
/* Follow toggles: flip aria-pressed and, when the button names a counter (data-follow="id"), add one follower.
Tabs: click or arrow keys / Home / End move between tabs; inactive panels are hidden only once this runs,
so without JavaScript both panels show one after the other. */
document.querySelectorAll('button[aria-pressed]').forEach(function (btn) {
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') !== 'true';
btn.setAttribute('aria-pressed', String(on));
var count = btn.dataset.follow && document.getElementById(btn.dataset.follow);
if (count) count.textContent = (Number(count.dataset.count) + (on ? 1 : 0)).toLocaleString('en-US');
});
});
document.querySelectorAll('[role=tablist]').forEach(function (list) {
var tabs = Array.prototype.slice.call(list.querySelectorAll('[role=tab]'));
function select(tab, focus) {
tabs.forEach(function (t) {
var on = t === tab;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1;
document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
});
if (focus) tab.focus();
}
list.addEventListener('click', function (e) { var t = e.target.closest('[role=tab]'); if (t) select(t); });
list.addEventListener('keydown', function (e) {
var i = tabs.indexOf(document.activeElement);
var next = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key];
if (i < 0 || next === undefined) return;
e.preventDefault();
select(tabs[(next + tabs.length) % tabs.length], true);
});
select(tabs.filter(function (t) { return t.getAttribute('aria-selected') === 'true'; })[0] || tabs[0]);
});
About this set
A social profile for creators and community sites, plus a suggestions list. The creator profile with tabs has a soft gradient cover, an abstract gradient avatar, a Follow toggle and a Message link, the name and handle, a bio, counts for posts, followers and following, and Posts and About tabs. The Posts tab shows six post tiles with titles over gradients, and About is a short description list with location, join date, gear and website. Who to follow lists three creators with their handle, follower count and their own Follow toggle. Follow is a button with aria-pressed: pressing it fills or clears the button, swaps a plus icon for a tick and, on the main card, adds one to the follower count. The label stays Follow in both states so the accessible name never changes, and the pressed state is announced. The tabs follow the ARIA tabs pattern with arrow keys, Home and End. About thirty lines of vanilla JavaScript run both, and in the plain CSS and Tailwind versions both tab panels show one after the other when JavaScript is off. The Bootstrap version uses its own tab and button plugins.
What’s included
Follow toggle with aria-pressed and a live follower count
Posts and About tabs with arrow key, Home and End navigation
Abstract gradient avatars and post tiles, no photos
Who to follow list with one toggle per creator
Bootstrap version uses .nav-underline tabs and data-bs-toggle="button"
Accessibility
Follow is a toggle button whose pressed state screen readers announce, and its name includes the creator's name. The tabs use role="tablist", role="tab" with aria-selected and aria-controls, and labelled tab panels; only the selected tab is in the Tab order and arrow keys move between tabs. All controls show a 3px focus outline, and transitions stop under prefers-reduced-motion.
Customise it
Change --cr-accent, --cr-accent-dark and --cr-tint on .cr for the button and tab colours, and the gradients on .cr-cover, .cr-av and the post tiles. Bootstrap maps the same variables onto --bs-btn-* and --bs-nav-* variables; Tailwind uses colour utilities such as bg-[#be185d].