UI Components Easy
Collapsible
A smooth expanding/collapsing content section with animated height transitions. Uses the CSS grid-template-rows trick for true 0-to-auto height animation.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
}
.collapsible-wrapper {
width: min(560px, 100%);
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.collapsible-heading {
font-size: 1.375rem;
font-weight: 700;
color: #f1f5f9;
}
.collapsible-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* ── Collapsible item ── */
.collapsible {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 0.875rem;
overflow: hidden;
transition: border-color 0.2s ease;
}
.collapsible.open {
border-color: rgba(99, 102, 241, 0.2);
}
/* ── Trigger ── */
.collapsible-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
background: transparent;
border: none;
color: inherit;
cursor: pointer;
text-align: left;
transition: background 0.15s ease;
}
.collapsible-trigger:hover {
background: rgba(255, 255, 255, 0.02);
}
.trigger-left {
display: flex;
align-items: center;
gap: 0.875rem;
min-width: 0;
}
.trigger-icon {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 0.5rem;
display: grid;
place-items: center;
}
.trigger-icon--blue {
background: rgba(59, 130, 246, 0.12);
color: #60a5fa;
}
.trigger-icon--purple {
background: rgba(168, 85, 247, 0.12);
color: #c084fc;
}
.trigger-icon--green {
background: rgba(34, 197, 94, 0.12);
color: #4ade80;
}
.trigger-left div:last-child {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.trigger-left strong {
font-size: 0.9rem;
font-weight: 600;
color: #e2e8f0;
}
.trigger-left span {
font-size: 0.78rem;
color: #64748b;
}
/* ── Chevron ── */
.collapsible-chevron {
flex-shrink: 0;
color: #475569;
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.15s ease;
}
.collapsible.open .collapsible-chevron {
transform: rotate(180deg);
color: #6366f1;
}
/* ── Panel — grid-template-rows trick ── */
.collapsible-panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.collapsible.open .collapsible-panel {
grid-template-rows: 1fr;
}
.collapsible-panel-inner {
overflow: hidden;
}
/* ── Panel content ── */
.panel-content {
padding: 0 1.25rem 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
padding-top: 1rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.form-field label {
font-size: 0.78rem;
font-weight: 500;
color: #94a3b8;
}
.fake-input {
font-size: 0.85rem;
color: #e2e8f0;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
}
.fake-input--tall {
min-height: 4rem;
line-height: 1.5;
}
.toggle-row {
display: flex;
align-items: center;
gap: 0.625rem;
}
.toggle-row span {
font-size: 0.85rem;
color: #94a3b8;
}
.fake-toggle {
width: 36px;
height: 20px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
position: relative;
transition: background 0.2s ease;
}
.fake-toggle::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #64748b;
transition: transform 0.2s ease, background 0.2s ease;
}
.fake-toggle.active {
background: rgba(99, 102, 241, 0.3);
}
.fake-toggle.active::after {
transform: translateX(16px);
background: #818cf8;
}
/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {
.collapsible-panel,
.collapsible-chevron {
transition: none;
}
}(function () {
"use strict";
var collapsibles = document.querySelectorAll("[data-collapsible]");
collapsibles.forEach(function (item) {
var trigger = item.querySelector(".collapsible-trigger");
var panel = item.querySelector(".collapsible-panel");
if (!trigger || !panel) return;
trigger.addEventListener("click", function () {
var isOpen = item.classList.contains("open");
if (isOpen) {
item.classList.remove("open");
trigger.setAttribute("aria-expanded", "false");
panel.setAttribute("aria-hidden", "true");
} else {
item.classList.add("open");
trigger.setAttribute("aria-expanded", "true");
panel.setAttribute("aria-hidden", "false");
}
});
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Collapsible</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="collapsible-wrapper">
<h2 class="collapsible-heading">Settings</h2>
<div class="collapsible-list">
<!-- Collapsible 1 - starts open -->
<div class="collapsible open" data-collapsible>
<button class="collapsible-trigger" aria-expanded="true">
<div class="trigger-left">
<div class="trigger-icon trigger-icon--blue">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
</div>
<div>
<strong>General Information</strong>
<span>Basic project details and metadata</span>
</div>
</div>
<svg class="collapsible-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="collapsible-panel" aria-hidden="false">
<div class="collapsible-panel-inner">
<div class="panel-content">
<div class="form-field">
<label>Project Name</label>
<div class="fake-input">My Awesome Project</div>
</div>
<div class="form-field">
<label>Description</label>
<div class="fake-input fake-input--tall">A brief description of your project that helps others understand what it does and why it exists.</div>
</div>
<div class="form-field">
<label>Visibility</label>
<div class="toggle-row">
<div class="fake-toggle active"></div>
<span>Public</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Collapsible 2 -->
<div class="collapsible" data-collapsible>
<button class="collapsible-trigger" aria-expanded="false">
<div class="trigger-left">
<div class="trigger-icon trigger-icon--purple">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<div>
<strong>Security</strong>
<span>Authentication and access control</span>
</div>
</div>
<svg class="collapsible-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="collapsible-panel" aria-hidden="true">
<div class="collapsible-panel-inner">
<div class="panel-content">
<div class="form-field">
<label>Two-Factor Auth</label>
<div class="toggle-row">
<div class="fake-toggle"></div>
<span>Disabled</span>
</div>
</div>
<div class="form-field">
<label>Session Timeout</label>
<div class="fake-input">30 minutes</div>
</div>
</div>
</div>
</div>
</div>
<!-- Collapsible 3 -->
<div class="collapsible" data-collapsible>
<button class="collapsible-trigger" aria-expanded="false">
<div class="trigger-left">
<div class="trigger-icon trigger-icon--green">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="m22 6-10 7L2 6"/></svg>
</div>
<div>
<strong>Notifications</strong>
<span>Email and push notification preferences</span>
</div>
</div>
<svg class="collapsible-chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="collapsible-panel" aria-hidden="true">
<div class="collapsible-panel-inner">
<div class="panel-content">
<div class="form-field">
<label>Email Notifications</label>
<div class="toggle-row">
<div class="fake-toggle active"></div>
<span>Enabled</span>
</div>
</div>
<div class="form-field">
<label>Push Notifications</label>
<div class="toggle-row">
<div class="fake-toggle active"></div>
<span>Enabled</span>
</div>
</div>
<div class="form-field">
<label>Weekly Digest</label>
<div class="toggle-row">
<div class="fake-toggle"></div>
<span>Disabled</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, ReactNode, useCallback } from "react";
interface CollapsibleProps {
trigger: ReactNode;
children: ReactNode;
defaultOpen?: boolean;
icon?: ReactNode;
iconBg?: string;
iconColor?: string;
title?: string;
subtitle?: string;
}
function Collapsible({
children,
defaultOpen = false,
icon,
iconBg = "rgba(59,130,246,0.12)",
iconColor = "#60a5fa",
title = "Section",
subtitle,
}: CollapsibleProps) {
const [open, setOpen] = useState(defaultOpen);
const toggle = useCallback(() => setOpen((prev) => !prev), []);
return (
<div
style={{
background: "rgba(255,255,255,0.03)",
border: `1px solid ${open ? "rgba(99,102,241,0.2)" : "rgba(255,255,255,0.07)"}`,
borderRadius: "0.875rem",
overflow: "hidden",
transition: "border-color 0.2s ease",
}}
>
{/* Trigger */}
<button
onClick={toggle}
aria-expanded={open}
style={{
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "1rem",
padding: "1rem 1.25rem",
background: "transparent",
border: "none",
color: "inherit",
cursor: "pointer",
textAlign: "left",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "0.875rem", minWidth: 0 }}>
{icon && (
<div
style={{
flexShrink: 0,
width: 36,
height: 36,
borderRadius: "0.5rem",
display: "grid",
placeItems: "center",
background: iconBg,
color: iconColor,
}}
>
{icon}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: "0.1rem" }}>
<strong style={{ fontSize: "0.9rem", fontWeight: 600, color: "#e2e8f0" }}>
{title}
</strong>
{subtitle && <span style={{ fontSize: "0.78rem", color: "#64748b" }}>{subtitle}</span>}
</div>
</div>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
style={{
flexShrink: 0,
color: open ? "#6366f1" : "#475569",
transform: open ? "rotate(180deg)" : "rotate(0)",
transition: "transform 0.35s cubic-bezier(0.34,1.56,0.64,1), color 0.15s ease",
}}
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
{/* Panel */}
<div
aria-hidden={!open}
style={{
display: "grid",
gridTemplateRows: open ? "1fr" : "0fr",
transition: "grid-template-rows 0.35s cubic-bezier(0.34,1.56,0.64,1)",
}}
>
<div style={{ overflow: "hidden" }}>
<div
style={{
padding: "0 1.25rem 1.25rem",
borderTop: "1px solid rgba(255,255,255,0.05)",
paddingTop: "1rem",
}}
>
{children}
</div>
</div>
</div>
</div>
);
}
// Demo
const InfoIcon = (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" />
</svg>
);
const ShieldIcon = (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
);
const MailIcon = (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<path d="m22 6-10 7L2 6" />
</svg>
);
export default function CollapsibleDemo() {
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
padding: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
color: "#f1f5f9",
}}
>
<div
style={{
width: "min(560px, 100%)",
display: "flex",
flexDirection: "column",
gap: "1.25rem",
}}
>
<h2 style={{ fontSize: "1.375rem", fontWeight: 700 }}>Settings</h2>
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<Collapsible
trigger={null}
defaultOpen
icon={InfoIcon}
iconBg="rgba(59,130,246,0.12)"
iconColor="#60a5fa"
title="General Information"
subtitle="Basic project details and metadata"
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div>
<label
style={{
fontSize: "0.78rem",
fontWeight: 500,
color: "#94a3b8",
display: "block",
marginBottom: "0.375rem",
}}
>
Project Name
</label>
<div
style={{
fontSize: "0.85rem",
color: "#e2e8f0",
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: "0.5rem",
padding: "0.5rem 0.75rem",
}}
>
My Awesome Project
</div>
</div>
<div>
<label
style={{
fontSize: "0.78rem",
fontWeight: 500,
color: "#94a3b8",
display: "block",
marginBottom: "0.375rem",
}}
>
Description
</label>
<div
style={{
fontSize: "0.85rem",
color: "#e2e8f0",
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: "0.5rem",
padding: "0.5rem 0.75rem",
minHeight: "4rem",
lineHeight: 1.5,
}}
>
A brief description of your project that helps others understand what it does and
why it exists.
</div>
</div>
</div>
</Collapsible>
<Collapsible
trigger={null}
icon={ShieldIcon}
iconBg="rgba(168,85,247,0.12)"
iconColor="#c084fc"
title="Security"
subtitle="Authentication and access control"
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div>
<label
style={{
fontSize: "0.78rem",
fontWeight: 500,
color: "#94a3b8",
display: "block",
marginBottom: "0.375rem",
}}
>
Two-Factor Auth
</label>
<span style={{ fontSize: "0.85rem", color: "#94a3b8" }}>Disabled</span>
</div>
<div>
<label
style={{
fontSize: "0.78rem",
fontWeight: 500,
color: "#94a3b8",
display: "block",
marginBottom: "0.375rem",
}}
>
Session Timeout
</label>
<div
style={{
fontSize: "0.85rem",
color: "#e2e8f0",
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: "0.5rem",
padding: "0.5rem 0.75rem",
}}
>
30 minutes
</div>
</div>
</div>
</Collapsible>
<Collapsible
trigger={null}
icon={MailIcon}
iconBg="rgba(34,197,94,0.12)"
iconColor="#4ade80"
title="Notifications"
subtitle="Email and push notification preferences"
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}
>
<span style={{ fontSize: "0.85rem", color: "#94a3b8" }}>Email Notifications</span>
<span style={{ fontSize: "0.78rem", color: "#4ade80" }}>Enabled</span>
</div>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}
>
<span style={{ fontSize: "0.85rem", color: "#94a3b8" }}>Push Notifications</span>
<span style={{ fontSize: "0.78rem", color: "#4ade80" }}>Enabled</span>
</div>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}
>
<span style={{ fontSize: "0.85rem", color: "#94a3b8" }}>Weekly Digest</span>
<span style={{ fontSize: "0.78rem", color: "#64748b" }}>Disabled</span>
</div>
</div>
</Collapsible>
</div>
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const sections = ref([
{
id: 1,
open: true,
title: "General Information",
subtitle: "Basic project details and metadata",
iconBg: "rgba(59,130,246,0.12)",
iconColor: "#60a5fa",
icon: "info",
},
{
id: 2,
open: false,
title: "Security",
subtitle: "Authentication and access control",
iconBg: "rgba(168,85,247,0.12)",
iconColor: "#c084fc",
icon: "shield",
},
{
id: 3,
open: false,
title: "Notifications",
subtitle: "Email and push notification preferences",
iconBg: "rgba(34,197,94,0.12)",
iconColor: "#4ade80",
icon: "mail",
},
]);
function toggle(id) {
const s = sections.value.find((s) => s.id === id);
if (s) s.open = !s.open;
}
</script>
<template>
<div style="min-height: 100vh; background: #0a0a0a; display: grid; place-items: center; padding: 2rem; font-family: system-ui, -apple-system, sans-serif; color: #f1f5f9;">
<div style="width: min(560px, 100%); display: flex; flex-direction: column; gap: 1.25rem;">
<h2 style="font-size: 1.375rem; font-weight: 700;">Settings</h2>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<div
v-for="section in sections"
:key="section.id"
:style="{
background: 'rgba(255,255,255,0.03)',
border: `1px solid ${section.open ? 'rgba(99,102,241,0.2)' : 'rgba(255,255,255,0.07)'}`,
borderRadius: '0.875rem',
overflow: 'hidden',
transition: 'border-color 0.2s ease',
}"
>
<!-- Trigger -->
<button
@click="toggle(section.id)"
:aria-expanded="section.open"
style="width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; background: transparent; border: none; color: inherit; cursor: pointer; text-align: left;"
>
<div style="display: flex; align-items: center; gap: 0.875rem; min-width: 0;">
<div :style="{ flexShrink: 0, width: '36px', height: '36px', borderRadius: '0.5rem', display: 'grid', placeItems: 'center', background: section.iconBg, color: section.iconColor }">
<svg v-if="section.icon === 'info'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16v-4M12 8h.01"/>
</svg>
<svg v-else-if="section.icon === 'shield'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<path d="m22 6-10 7L2 6"/>
</svg>
</div>
<div style="display: flex; flex-direction: column; gap: 0.1rem;">
<strong style="font-size: 0.9rem; font-weight: 600; color: #e2e8f0;">{{ section.title }}</strong>
<span v-if="section.subtitle" style="font-size: 0.78rem; color: #64748b;">{{ section.subtitle }}</span>
</div>
</div>
<svg
width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
:style="{ flexShrink: 0, color: section.open ? '#6366f1' : '#475569', transform: `rotate(${section.open ? 180 : 0}deg)`, transition: 'transform 0.35s cubic-bezier(0.34,1.56,0.64,1), color 0.15s ease' }"
>
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<!-- Panel -->
<div
:aria-hidden="!section.open"
:style="{ display: 'grid', gridTemplateRows: section.open ? '1fr' : '0fr', transition: 'grid-template-rows 0.35s cubic-bezier(0.34,1.56,0.64,1)' }"
>
<div style="overflow: hidden;">
<div style="padding: 0 1.25rem 1.25rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1rem;">
<!-- General Information -->
<div v-if="section.id === 1" style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Project Name</label>
<div style="font-size: 0.85rem; color: #e2e8f0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.5rem; padding: 0.5rem 0.75rem;">
My Awesome Project
</div>
</div>
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Description</label>
<div style="font-size: 0.85rem; color: #e2e8f0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.5rem; padding: 0.5rem 0.75rem; min-height: 4rem; line-height: 1.5;">
A brief description of your project that helps others understand what it does and why it exists.
</div>
</div>
</div>
<!-- Security -->
<div v-else-if="section.id === 2" style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Two-Factor Auth</label>
<span style="font-size: 0.85rem; color: #94a3b8;">Disabled</span>
</div>
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Session Timeout</label>
<div style="font-size: 0.85rem; color: #e2e8f0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.5rem; padding: 0.5rem 0.75rem;">
30 minutes
</div>
</div>
</div>
<!-- Notifications -->
<div v-else style="display: flex; flex-direction: column; gap: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.85rem; color: #94a3b8;">Email Notifications</span>
<span style="font-size: 0.78rem; color: #4ade80;">Enabled</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.85rem; color: #94a3b8;">Push Notifications</span>
<span style="font-size: 0.78rem; color: #4ade80;">Enabled</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.85rem; color: #94a3b8;">Weekly Digest</span>
<span style="font-size: 0.78rem; color: #64748b;">Disabled</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template><script>
let sections = [
{
id: 1,
open: true,
title: "General Information",
subtitle: "Basic project details and metadata",
iconBg: "rgba(59,130,246,0.12)",
iconColor: "#60a5fa",
icon: "info",
},
{
id: 2,
open: false,
title: "Security",
subtitle: "Authentication and access control",
iconBg: "rgba(168,85,247,0.12)",
iconColor: "#c084fc",
icon: "shield",
},
{
id: 3,
open: false,
title: "Notifications",
subtitle: "Email and push notification preferences",
iconBg: "rgba(34,197,94,0.12)",
iconColor: "#4ade80",
icon: "mail",
},
];
function toggle(id) {
sections = sections.map((s) => (s.id === id ? { ...s, open: !s.open } : s));
}
</script>
<div style="min-height: 100vh; background: #0a0a0a; display: grid; place-items: center; padding: 2rem; font-family: system-ui, -apple-system, sans-serif; color: #f1f5f9;">
<div style="width: min(560px, 100%); display: flex; flex-direction: column; gap: 1.25rem;">
<h2 style="font-size: 1.375rem; font-weight: 700;">Settings</h2>
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
{#each sections as section (section.id)}
<div style="background: rgba(255,255,255,0.03); border: 1px solid {section.open ? 'rgba(99,102,241,0.2)' : 'rgba(255,255,255,0.07)'}; border-radius: 0.875rem; overflow: hidden; transition: border-color 0.2s ease;">
<!-- Trigger -->
<button
on:click={() => toggle(section.id)}
aria-expanded={section.open}
style="width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; background: transparent; border: none; color: inherit; cursor: pointer; text-align: left;"
>
<div style="display: flex; align-items: center; gap: 0.875rem; min-width: 0;">
<div style="flex-shrink: 0; width: 36px; height: 36px; border-radius: 0.5rem; display: grid; place-items: center; background: {section.iconBg}; color: {section.iconColor};">
{#if section.icon === 'info'}
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16v-4M12 8h.01"/>
</svg>
{:else if section.icon === 'shield'}
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
{:else}
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<path d="m22 6-10 7L2 6"/>
</svg>
{/if}
</div>
<div style="display: flex; flex-direction: column; gap: 0.1rem;">
<strong style="font-size: 0.9rem; font-weight: 600; color: #e2e8f0;">{section.title}</strong>
{#if section.subtitle}
<span style="font-size: 0.78rem; color: #64748b;">{section.subtitle}</span>
{/if}
</div>
</div>
<svg
width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
style="flex-shrink: 0; color: {section.open ? '#6366f1' : '#475569'}; transform: rotate({section.open ? 180 : 0}deg); transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1), color 0.15s ease;"
>
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<!-- Panel -->
<div
aria-hidden={!section.open}
style="display: grid; grid-template-rows: {section.open ? '1fr' : '0fr'}; transition: grid-template-rows 0.35s cubic-bezier(0.34,1.56,0.64,1);"
>
<div style="overflow: hidden;">
<div style="padding: 0 1.25rem 1.25rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1rem;">
{#if section.id === 1}
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Project Name</label>
<div style="font-size: 0.85rem; color: #e2e8f0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.5rem; padding: 0.5rem 0.75rem;">
My Awesome Project
</div>
</div>
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Description</label>
<div style="font-size: 0.85rem; color: #e2e8f0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.5rem; padding: 0.5rem 0.75rem; min-height: 4rem; line-height: 1.5;">
A brief description of your project that helps others understand what it does and why it exists.
</div>
</div>
</div>
{:else if section.id === 2}
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Two-Factor Auth</label>
<span style="font-size: 0.85rem; color: #94a3b8;">Disabled</span>
</div>
<div>
<label style="font-size: 0.78rem; font-weight: 500; color: #94a3b8; display: block; margin-bottom: 0.375rem;">Session Timeout</label>
<div style="font-size: 0.85rem; color: #e2e8f0; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 0.5rem; padding: 0.5rem 0.75rem;">
30 minutes
</div>
</div>
</div>
{:else}
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.85rem; color: #94a3b8;">Email Notifications</span>
<span style="font-size: 0.78rem; color: #4ade80;">Enabled</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.85rem; color: #94a3b8;">Push Notifications</span>
<span style="font-size: 0.78rem; color: #4ade80;">Enabled</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 0.85rem; color: #94a3b8;">Weekly Digest</span>
<span style="font-size: 0.78rem; color: #64748b;">Disabled</span>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
</div>Collapsible
A smooth expanding/collapsing content section that animates between zero and auto height. Uses the CSS grid-template-rows: 0fr / 1fr technique for a true height transition without JavaScript measurements or max-height hacks.
How it works
- The collapsible content is wrapped in a grid container.
grid-template-rows: 0frcollapses the content to zero height.- Toggling to
1frsmoothly expands to the content’s natural height. - The inner wrapper uses
overflow: hiddento clip during the transition.
Features
- True 0-to-auto height animation via CSS grid
- Animated chevron rotation
- Multiple independent collapsibles
- Keyboard accessible trigger button
- Respects
prefers-reduced-motion