Skip to content

Custom animation

Animate the expander part with the --opening and --closing states. Keep grid-template-rows: 1fr during closing when the keyframes do not animate rows.

Incoming signal

Kepler relay / A–17

“The quietest route is clear. Follow the blue markers until the second sunrise.”

Origin
Kepler-186f
Distance
582 ly
Integrity
96.4%

Code

The following example assumes that you have imported the component and set up the theme.

Custom collapsible animation
<div class="signal-console-example">
<button type="button" class="signal-trigger" aria-controls="signal-transmission" aria-expanded="false">
<span class="signal-pulse" aria-hidden="true"></span>
Decode transmission
<span class="signal-code" aria-hidden="true">A–17</span>
</button>
<sv-collapsible id="signal-transmission" class="signal-collapsible">
84 collapsed lines
<article class="signal-panel">
<header>
<div>
<span class="eyebrow">Incoming signal</span>
<h2>Kepler relay / A–17</h2>
</div>
</header>
<p class="signal-message">“The quietest route is clear. Follow the blue markers until the second sunrise.”</p>
<dl class="signal-data">
<div>
<dt>Origin</dt>
<dd>Kepler-186f</dd>
</div>
<div>
<dt>Distance</dt>
<dd>582 ly</dd>
</div>
<div>
<dt>Integrity</dt>
<dd>96.4%</dd>
</div>
</dl>
</article>
</sv-collapsible>
</div>
<script>
const container = document.querySelector(".signal-console-example");
const trigger = container.querySelector(".signal-trigger");
const collapsible = container.querySelector("sv-collapsible");
trigger.addEventListener("click", () => {
collapsible.toggle();
trigger.ariaExpanded = String(collapsible.opened);
});
</script>
<style>
.signal-console-example {
inline-size: min(34rem, 100%);
padding: 1rem;
margin: auto;
overflow: hidden;
font-family: ui-monospace, "Cascadia Code", monospace;
background:
linear-gradient(color-mix(in srgb, var(--sv-accent) 5%, transparent) 1px, transparent 1px),
linear-gradient(90deg, color-mix(in srgb, var(--sv-accent) 5%, transparent) 1px, transparent 1px), var(--sv-gray-8);
background-size: 1.25rem 1.25rem;
.signal-trigger {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.75em;
align-items: center;
inline-size: 100%;
padding: 0.8em 1em;
font: inherit;
font-weight: 700;
color: var(--sv-gray-1);
text-align: start;
cursor: pointer;
background: color-mix(in srgb, var(--sv-gray-7) 88%, transparent);
border: solid var(--sv-bdr-sz-sm) var(--sv-gray-5);
border-radius: var(--sv-bdr-rad-md);
transition: border-color 350ms ease;
.signal-pulse {
inline-size: 0.65em;
block-size: 0.65em;
background: var(--sv-accent);
border-radius: 50%;
box-shadow: 0 0 0.75em var(--sv-accent);
}
.signal-code {
font-size: 0.75em;
font-weight: 500;
color: var(--sv-gray-3);
letter-spacing: 0.12em;
}
}
&:has(.signal-collapsible:state(--open)) .signal-trigger {
border-color: var(--sv-accent);
}
/* Phase states allow each direction to use its own timing and keyframes. */
.signal-collapsible {
--sv-open-dur: 350ms;
--sv-close-dur: 450ms;
--signal-enter-ease: --ease-out-back(2);
--signal-exit-ease: cubic-bezier(0.16, 1, 0.3, 1);
&:state(--opening)::part(expander) {
animation: signal-unfold var(--sv-open-dur) var(--signal-enter-ease);
}
&:state(--closing)::part(expander) {
animation: signal-fold var(--sv-close-dur) var(--signal-exit-ease);
}
/* Custom animations must explicitly honor reduced-motion preferences. */
@media (prefers-reduced-motion: reduce) {
--sv-open-dur: 0s;
--sv-close-dur: 0s;
}
}
85 collapsed lines
.signal-panel {
position: relative;
padding: 1.25em;
margin-block-start: 0.65rem;
color: var(--sv-gray-2);
background:
linear-gradient(135deg, color-mix(in srgb, var(--sv-accent) 13%, transparent), transparent 45%),
color-mix(in srgb, var(--sv-gray-7) 94%, transparent);
border: solid var(--sv-bdr-sz-sm) color-mix(in srgb, var(--sv-accent) 38%, var(--sv-gray-5));
border-radius: var(--sv-bdr-rad-md);
&::before {
position: absolute;
inset-block: 0;
inset-inline-start: 1.25em;
inline-size: 1px;
content: "";
background: linear-gradient(transparent, var(--sv-accent), transparent);
opacity: 0.35;
}
header {
display: flex;
gap: 1em;
align-items: start;
justify-content: space-between;
padding-inline-start: 1em;
}
.eyebrow,
dt {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
}
.eyebrow {
color: var(--sv-accent);
}
h2 {
margin: 0.25em 0 0;
font-size: 1.05rem;
color: var(--sv-gray-1);
}
.signal-message {
padding: 1em;
margin-block: 1em;
font-family: system-ui, sans-serif;
font-size: 1.05rem;
font-style: italic;
line-height: 1.55;
background: color-mix(in srgb, var(--sv-gray-8) 55%, transparent);
border-inline-start: solid 2px var(--sv-accent);
}
.signal-data {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 0;
div {
min-inline-size: 0;
padding: 0.65em;
background: color-mix(in srgb, var(--sv-gray-8) 45%, transparent);
border-radius: var(--sv-bdr-rad-sm);
}
dt {
color: var(--sv-gray-3);
}
dd {
margin: 0.25em 0 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.8rem;
color: var(--sv-gray-1);
white-space: nowrap;
}
}
}
}
/* Grid rows animate the occupied height; the other properties shape the visual reveal. */
@keyframes signal-unfold {
from {
grid-template-rows: 0fr;
opacity: 0;
filter: blur(10px);
transform: translateY(-1rem) scale(0.9);
}
to {
grid-template-rows: 1fr;
opacity: 1;
filter: blur(0);
transform: translateY(0) scale(1);
}
}
@keyframes signal-fold {
from {
grid-template-rows: 1fr;
opacity: 1;
filter: blur(0);
clip-path: inset(0);
transform: translateY(0) scaleY(1);
}
to {
grid-template-rows: 0fr;
opacity: 0;
filter: blur(8px);
clip-path: inset(0 0 100%);
transform: translateY(-0.5rem) scaleY(0.94);
}
}
</style>