06Shipping fields that fold away and fill themselves in
Turning on same as orderer folds the shipping fieldset into a single summary line that already carries the orderer's name and address.
Best forOrder forms that collect orderer and shipping separately, billing address forms
grid-template-rowsaria-hiddeninert

<main class="stage">
<section class="sa is-demo" id="sa" aria-label="Shipping address form">
<div class="sa__who" data-tel="010-2345-6789">
<p class="sa__kicker">Orderer</p>
<p class="sa__line">
<span class="sa__ic" aria-hidden="true"><svg viewBox="0 0 256 256" fill="currentColor"><path d="M192,96a64,64,0,1,1-64-64A64,64,0,0,1,192,96Z" opacity="0.2"/><path d="M230.92,212c-15.23-26.33-38.7-45.21-66.09-54.16a72,72,0,1,0-73.66,0C63.78,166.78,40.31,185.66,25.08,212a8,8,0,1,0,13.85,8c18.84-32.56,52.14-52,89.07-52s70.23,19.44,89.07,52a8,8,0,1,0,13.85-8ZM72,96a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z"/></svg></span>
<b class="sa__v" id="saWhoName">Kim Doyun</b>
</p>
<p class="sa__line">
<span class="sa__ic" aria-hidden="true"><svg viewBox="0 0 256 256" fill="currentColor"><path d="M216,120v96H152V152H104v64H40V120a8,8,0,0,1,2.34-5.66l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,216,120Z" opacity="0.2"/><path d="M219.31,108.68l-80-80a16,16,0,0,0-22.62,0l-80,80A15.87,15.87,0,0,0,32,120v96a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V160h32v56a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V120A15.87,15.87,0,0,0,219.31,108.68ZM208,208H160V152a8,8,0,0,0-8-8H104a8,8,0,0,0-8,8v56H48V120l80-80,80,80Z"/></svg></span>
<span class="sa__v" id="saWhoAddr">12 Yanghwa-ro, Mapo-gu, Seoul, 3F</span>
</p>
</div>
<label class="sa__same">
<input class="sa__box" type="checkbox" id="saSame" checked>
<span class="sa__tick" aria-hidden="true"><svg viewBox="0 0 256 256" fill="currentColor"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg></span>
<span class="sa__label">Same as orderer</span>
</label>
<!-- 접혔을 때 남는 요약 한 줄 — 값은 JS 가 Orderer 칸에서 그대로 옮겨 적는다 -->
<div class="sa__sumfold">
<p class="sa__sum"><b class="sa__sumtext" id="saSum">Kim Doyun · 12 Yanghwa-ro, Mapo-gu, Seoul, 3F</b></p>
</div>
<div class="sa__fold" id="saFold" aria-hidden="true" inert>
<div class="sa__inner">
<p class="sa__field">
<label class="sa__fk" for="saName">Recipient</label>
<input class="sa__fv" id="saName" type="text" value="Kim Doyun">
</p>
<p class="sa__field">
<label class="sa__fk" for="saAddr">Address</label>
<input class="sa__fv" id="saAddr" type="text" value="12 Yanghwa-ro, Mapo-gu, Seoul, 3F">
</p>
<p class="sa__field">
<label class="sa__fk" for="saTel">Phone</label>
<input class="sa__fv" id="saTel" type="tel" value="010-2345-6789">
</p>
</div>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 주문자와 같음을 켜면 배송지 칸 묶음이 grid-template-rows 로 접히고 요약 한 줄만 남는다.
// 접히는 칸에는 세로 패딩을 두지 않는다 — 0fr 에서도 패딩은 남아 칸이 완전히 닫히지 않는다
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
.sa {
box-sizing: border-box;
width: min(300px, 92vw);
padding: $sp-3;
border: 1px solid rgba($subject-ink, .14);
border-radius: $r-card;
background: $subject-cream;
color: $subject-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.sa * { box-sizing: border-box; }
.sa__who { margin-bottom: $sp-2; }
.sa__kicker {
margin: 0;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
letter-spacing: .04em;
color: rgba($subject-ink, .72);
}
.sa__line {
display: flex;
align-items: center;
gap: $sp-2;
margin: 0;
min-width: 0;
}
.sa__ic {
flex: 0 0 auto;
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
color: rgba($subject-ink, .72);
}
.sa__ic svg { width: 14px; height: 14px; }
.sa__v {
font-size: 12px;
font-weight: 500;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// ── 「주문자와 같음」 체크 ──
.sa__same {
display: flex;
align-items: center;
gap: $sp-2;
margin-bottom: $sp-2;
cursor: pointer;
}
.sa__box {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
.sa__tick {
position: relative;
flex: 0 0 auto;
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
border: 2px solid rgba($subject-ink, .3);
border-radius: $r-xs;
color: $subject-cream;
}
// 켜진 바탕은 따로 그린 겹 — opacity 만 켜므로 미리보기 루프에서 컷으로 갈아 끼워도 색이 섞이지 않는다
.sa__tick::before {
content: "";
position: absolute;
inset: -2px;
border: 2px solid $color;
border-radius: $r-xs;
background: $color;
opacity: 0;
transition: opacity $dur-instant linear;
}
.sa__tick svg {
position: relative;
width: 12px;
height: 12px;
opacity: 0;
transform: scale(.4);
transition: opacity $dur-instant linear, transform $dur-quick $easing;
}
.sa:has(.sa__box:checked) .sa__tick::before { opacity: 1; }
.sa:has(.sa__box:checked) .sa__tick svg { opacity: 1; transform: scale(1); }
.sa:has(.sa__box:focus-visible) .sa__tick { outline: 2px solid $color; outline-offset: 2px; }
.sa__label {
font-size: 12px;
font-weight: 700;
line-height: 1.3;
}
// ── 접혔을 때 남는 요약 한 줄 ──
.sa__sumfold {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $duration $easing;
}
.sa:has(.sa__box:checked) .sa__sumfold { grid-template-rows: 1fr; }
.sa__sum {
min-height: 0;
margin: 0;
overflow: hidden;
}
.sa__sumtext {
display: block;
border-left: 2px solid $color;
padding-left: $sp-2;
font-size: 11px;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// ── 배송지 입력 칸 묶음 ──
.sa__fold {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows $duration $easing;
}
.sa:has(.sa__box:checked) .sa__fold { grid-template-rows: 0fr; }
.sa__inner {
display: grid;
gap: $sp-1;
min-height: 0;
overflow: hidden;
}
.sa__field {
display: flex;
align-items: center;
gap: $sp-2;
margin: 0;
}
.sa__fk {
flex: 0 0 auto;
width: 48px;
font-size: 10px;
font-weight: 700;
line-height: 1.4;
color: rgba($subject-ink, .72);
}
.sa__fv {
flex: 1 1 auto;
min-width: 0;
height: 20px;
padding: 0 $sp-2;
border: 1px solid rgba($subject-ink, .2);
border-radius: $r-xs;
background: $stage-paper;
color: $subject-ink;
font-family: inherit;
font-size: 10px;
font-weight: 500;
line-height: 1.4;
}
.sa__fv:focus-visible { outline: 2px solid $color; outline-offset: 1px; }
// ── 격자 미리보기 루프: 접힘 → 펼침 → 접힘. 칸 높이는 컷, 곁들이는 움직임은 transform ──
@keyframes sa-fields {
0%, 40% { grid-template-rows: 0fr; }
44%, 80% { grid-template-rows: 1fr; }
84%, 100% { grid-template-rows: 0fr; }
}
@keyframes sa-sumrow {
0%, 40% { grid-template-rows: 1fr; }
44%, 80% { grid-template-rows: 0fr; }
84%, 100% { grid-template-rows: 1fr; }
}
@keyframes sa-tickbg {
0%, 40% { opacity: 1; }
44%, 80% { opacity: 0; }
84%, 100% { opacity: 1; }
}
@keyframes sa-tickfade {
0%, 40% { opacity: 1; }
44%, 80% { opacity: 0; }
84%, 100% { opacity: 1; }
}
// 컷만으로는 움직인 프레임이 두어 장뿐이라 측정이 안 찬다 — 체크·칸·요약이 저마다 한 번 올라서며 채운다
@keyframes sa-tickpop {
0%, 43% { transform: scale(1); }
44%, 84% { transform: scale(.3); }
96%, 100% { transform: scale(1); }
}
@keyframes sa-rise {
0%, 43% { transform: translateY(0); }
44% { transform: translateY(16px); }
76%, 100% { transform: translateY(0); }
}
// 요약 줄만 옆에서 들어온다 — 세로로 올리면 제 칸(overflow: hidden)에 반쯤 잘린 장이 대표 이미지로 뽑힌다
@keyframes sa-sumrise {
0%, 83% { transform: translateX(0); }
84% { transform: translateX(14px); }
100% { transform: translateX(0); }
}
.sa.is-demo {
.sa__fold,
.sa__sumfold,
.sa__tick::before {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.sa__fold { animation-name: sa-fields; }
.sa__sumfold { animation-name: sa-sumrow; }
.sa__tick::before { animation-name: sa-tickbg; }
// 체크 표시는 켜짐/꺼짐만 컷하고 크기는 부드럽게 — 투명도가 섞이지 않으니 유령 프레임이 안 생긴다
.sa__tick svg {
animation-name: sa-tickfade, sa-tickpop;
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: steps(1, end), $ease-out;
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.sa__field,
.sa__sumtext {
animation-duration: $dur-loop;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.sa__field { animation-name: sa-rise; }
.sa__field:nth-child(2) { animation-delay: 60ms; }
.sa__field:nth-child(3) { animation-delay: 120ms; }
.sa__sumtext { animation-name: sa-sumrise; }
}
@media (max-width: 340px) {
.sa { padding: $sp-2; }
.sa__kicker { font-size: 10px; }
.sa__v { font-size: 11px; }
.sa__label { font-size: 11px; }
.sa__sumtext { font-size: 10px; }
.sa__fv { height: 18px; }
.sa__fk { width: 44px; }
}
@media (prefers-reduced-motion: reduce) {
// 정지 상태에서도 「켜져 있고 접혀 있다」 는 결과는 보인다 — 움직임만 뺀다
.sa.is-demo .sa__fold,
.sa.is-demo .sa__sumfold,
.sa.is-demo .sa__tick::before,
.sa.is-demo .sa__tick svg,
.sa.is-demo .sa__field,
.sa.is-demo .sa__sumtext { animation: none !important; }
.sa.is-demo .sa__field,
.sa.is-demo .sa__sumtext { transform: none !important; }
.sa.is-demo .sa__tick svg { transform: scale(1) !important; opacity: 1 !important; }
.sa__fold { transition: none !important; }
.sa__sumfold { transition: none !important; }
.sa__tick::before { transition: none !important; }
.sa__tick svg { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sa {
box-sizing: border-box;
width: min(300px, 92vw);
padding: 12px;
border: 1px solid rgba(23, 20, 26, 0.14);
border-radius: 16px;
background: #fff7e6;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.sa * {
box-sizing: border-box;
}
.sa__who {
margin-bottom: 8px;
}
.sa__kicker {
margin: 0;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
letter-spacing: 0.04em;
color: rgba(23, 20, 26, 0.72);
}
.sa__line {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
min-width: 0;
}
.sa__ic {
flex: 0 0 auto;
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
color: rgba(23, 20, 26, 0.72);
}
.sa__ic svg {
width: 14px;
height: 14px;
}
.sa__v {
font-size: 12px;
font-weight: 500;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sa__same {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.sa__box {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
.sa__tick {
position: relative;
flex: 0 0 auto;
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
border: 2px solid rgba(23, 20, 26, 0.3);
border-radius: 4px;
color: #fff7e6;
}
.sa__tick::before {
content: "";
position: absolute;
inset: -2px;
border: 2px solid #2f6df6;
border-radius: 4px;
background: #2f6df6;
opacity: 0;
transition: opacity 140ms linear;
}
.sa__tick svg {
position: relative;
width: 12px;
height: 12px;
opacity: 0;
transform: scale(0.4);
transition: opacity 140ms linear, transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sa:has(.sa__box:checked) .sa__tick::before {
opacity: 1;
}
.sa:has(.sa__box:checked) .sa__tick svg {
opacity: 1;
transform: scale(1);
}
.sa:has(.sa__box:focus-visible) .sa__tick {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.sa__label {
font-size: 12px;
font-weight: 700;
line-height: 1.3;
}
.sa__sumfold {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sa:has(.sa__box:checked) .sa__sumfold {
grid-template-rows: 1fr;
}
.sa__sum {
min-height: 0;
margin: 0;
overflow: hidden;
}
.sa__sumtext {
display: block;
border-left: 2px solid #2f6df6;
padding-left: 8px;
font-size: 11px;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sa__fold {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.sa:has(.sa__box:checked) .sa__fold {
grid-template-rows: 0fr;
}
.sa__inner {
display: grid;
gap: 4px;
min-height: 0;
overflow: hidden;
}
.sa__field {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.sa__fk {
flex: 0 0 auto;
width: 48px;
font-size: 10px;
font-weight: 700;
line-height: 1.4;
color: rgba(23, 20, 26, 0.72);
}
.sa__fv {
flex: 1 1 auto;
min-width: 0;
height: 20px;
padding: 0 8px;
border: 1px solid rgba(23, 20, 26, 0.2);
border-radius: 4px;
background: #ffffff;
color: #17141a;
font-family: inherit;
font-size: 10px;
font-weight: 500;
line-height: 1.4;
}
.sa__fv:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 1px;
}
@keyframes sa-fields {
0%, 40% {
grid-template-rows: 0fr;
}
44%, 80% {
grid-template-rows: 1fr;
}
84%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes sa-sumrow {
0%, 40% {
grid-template-rows: 1fr;
}
44%, 80% {
grid-template-rows: 0fr;
}
84%, 100% {
grid-template-rows: 1fr;
}
}
@keyframes sa-tickbg {
0%, 40% {
opacity: 1;
}
44%, 80% {
opacity: 0;
}
84%, 100% {
opacity: 1;
}
}
@keyframes sa-tickfade {
0%, 40% {
opacity: 1;
}
44%, 80% {
opacity: 0;
}
84%, 100% {
opacity: 1;
}
}
@keyframes sa-tickpop {
0%, 43% {
transform: scale(1);
}
44%, 84% {
transform: scale(0.3);
}
96%, 100% {
transform: scale(1);
}
}
@keyframes sa-rise {
0%, 43% {
transform: translateY(0);
}
44% {
transform: translateY(16px);
}
76%, 100% {
transform: translateY(0);
}
}
@keyframes sa-sumrise {
0%, 83% {
transform: translateX(0);
}
84% {
transform: translateX(14px);
}
100% {
transform: translateX(0);
}
}
.sa.is-demo .sa__fold,
.sa.is-demo .sa__sumfold,
.sa.is-demo .sa__tick::before {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.sa.is-demo .sa__fold {
animation-name: sa-fields;
}
.sa.is-demo .sa__sumfold {
animation-name: sa-sumrow;
}
.sa.is-demo .sa__tick::before {
animation-name: sa-tickbg;
}
.sa.is-demo .sa__tick svg {
animation-name: sa-tickfade, sa-tickpop;
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), ease-out;
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.sa.is-demo .sa__field,
.sa.is-demo .sa__sumtext {
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.sa.is-demo .sa__field {
animation-name: sa-rise;
}
.sa.is-demo .sa__field:nth-child(2) {
animation-delay: 60ms;
}
.sa.is-demo .sa__field:nth-child(3) {
animation-delay: 120ms;
}
.sa.is-demo .sa__sumtext {
animation-name: sa-sumrise;
}
@media (max-width: 340px) {
.sa {
padding: 8px;
}
.sa__kicker {
font-size: 10px;
}
.sa__v {
font-size: 11px;
}
.sa__label {
font-size: 11px;
}
.sa__sumtext {
font-size: 10px;
}
.sa__fv {
height: 18px;
}
.sa__fk {
width: 44px;
}
}
@media (prefers-reduced-motion: reduce) {
.sa.is-demo .sa__fold,
.sa.is-demo .sa__sumfold,
.sa.is-demo .sa__tick::before,
.sa.is-demo .sa__tick svg,
.sa.is-demo .sa__field,
.sa.is-demo .sa__sumtext {
animation: none !important;
}
.sa.is-demo .sa__field,
.sa.is-demo .sa__sumtext {
transform: none !important;
}
.sa.is-demo .sa__tick svg {
transform: scale(1) !important;
opacity: 1 !important;
}
.sa__fold {
transition: none !important;
}
.sa__sumfold {
transition: none !important;
}
.sa__tick::before {
transition: none !important;
}
.sa__tick svg {
transition: none !important;
}
}// Same as orderer — 배송지 칸 묶음을 접고, 접힌 칸은 초점에서 뺀다(inert). 값 복사도 여기서
var sa = document.getElementById('sa');
var same = document.getElementById('saSame');
var fold = document.getElementById('saFold');
var who = sa.querySelector('.sa__who');
var whoName = document.getElementById('saWhoName');
var whoAddr = document.getElementById('saWhoAddr');
var sum = document.getElementById('saSum');
function syncShipping() {
sa.classList.remove('is-demo'); // 사람이 손대면 미리보기 루프는 끝난다
var folded = same.checked;
fold.setAttribute('aria-hidden', folded ? 'true' : 'false');
fold.toggleAttribute('inert', folded); // 접힌 칸은 탭으로도 안 들어간다
if (folded) {
document.getElementById('saName').value = whoName.textContent.trim();
document.getElementById('saAddr').value = whoAddr.textContent.trim();
document.getElementById('saTel').value = who.getAttribute('data-tel');
sum.textContent = whoName.textContent.trim() + ' · ' + whoAddr.textContent.trim();
}
}
same.addEventListener('change', syncShipping);import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ShipSameAddressCollapse.module.scss";
export type ShipSameAddressCollapseProps = {
kicker?: string;
ordererName?: string;
ordererAddress?: string;
ordererTel?: string;
sameLabel?: string;
nameLabel?: string;
addressLabel?: string;
telLabel?: string;
groupLabel?: string;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
// ambient 가 얇아 이벤트 타입을 못 쓴다 — 필요한 꼴만 로컬 타입으로 선언
type CheckChange = { target: { checked: boolean } };
type FieldChange = { target: { value: string } };
function UserIcon() {
return (
<svg viewBox="0 0 256 256" fill="currentColor"><path d="M192,96a64,64,0,1,1-64-64A64,64,0,0,1,192,96Z" opacity="0.2"/><path d="M230.92,212c-15.23-26.33-38.7-45.21-66.09-54.16a72,72,0,1,0-73.66,0C63.78,166.78,40.31,185.66,25.08,212a8,8,0,1,0,13.85,8c18.84-32.56,52.14-52,89.07-52s70.23,19.44,89.07,52a8,8,0,1,0,13.85-8ZM72,96a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z"/></svg>
);
}
function HouseIcon() {
return (
<svg viewBox="0 0 256 256" fill="currentColor"><path d="M216,120v96H152V152H104v64H40V120a8,8,0,0,1,2.34-5.66l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,216,120Z" opacity="0.2"/><path d="M219.31,108.68l-80-80a16,16,0,0,0-22.62,0l-80,80A15.87,15.87,0,0,0,32,120v96a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V160h32v56a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V120A15.87,15.87,0,0,0,219.31,108.68ZM208,208H160V152a8,8,0,0,0-8-8H104a8,8,0,0,0-8,8v56H48V120l80-80,80,80Z"/></svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 256 256" fill="currentColor"><path d="M232,56V200a16,16,0,0,1-16,16H40a16,16,0,0,1-16-16V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56Z" opacity="0.2"/><path d="M205.66,85.66l-96,96a8,8,0,0,1-11.32,0l-40-40a8,8,0,0,1,11.32-11.32L104,164.69l90.34-90.35a8,8,0,0,1,11.32,11.32Z"/></svg>
);
}
export default function ShipSameAddressCollapse({
kicker = "주문자",
ordererName = "김도윤",
ordererAddress = "서울 마포구 양화로 12, 3층",
ordererTel = "010-2345-6789",
sameLabel = "주문자와 같음",
nameLabel = "받는 분",
addressLabel = "주소",
telLabel = "연락처",
groupLabel = "배송지 입력",
demo = true,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: ShipSameAddressCollapseProps) {
const [same, setSame] = useState(true);
const [live, setLive] = useState(demo);
const [shipName, setShipName] = useState(ordererName);
const [shipAddress, setShipAddress] = useState(ordererAddress);
const [shipTel, setShipTel] = useState(ordererTel);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
// 주문자와 같음 — 값 복사는 여기서, 접힌 칸은 aria-hidden + inert 로 초점에서 뺀다
function onSame(e: CheckChange) {
const folded = e.target.checked;
setLive(false);
setSame(folded);
if (folded) {
setShipName(ordererName);
setShipAddress(ordererAddress);
setShipTel(ordererTel);
}
}
const cls = [styles.sa, live ? styles.isDemo : "", same ? styles.isSame : ""].filter(Boolean).join(" ");
const summary = `${ordererName} · ${ordererAddress}`;
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<div className={styles.who}>
<p className={styles.kicker}>{kicker}</p>
<p className={styles.line}>
<span className={styles.ic} aria-hidden="true"><UserIcon /></span>
<b className={styles.v}>{ordererName}</b>
</p>
<p className={styles.line}>
<span className={styles.ic} aria-hidden="true"><HouseIcon /></span>
<span className={styles.v}>{ordererAddress}</span>
</p>
</div>
<label className={styles.same}>
<input className={styles.box} type="checkbox" checked={same} onChange={onSame} />
<span className={styles.tick} aria-hidden="true"><CheckIcon /></span>
<span className={styles.label}>{sameLabel}</span>
</label>
{/* 접혔을 때 남는 요약 한 줄 — 주문자 이름·주소가 이미 들어와 있다 */}
<div className={styles.sumfold}>
<p className={styles.sum}><b className={styles.sumtext}>{summary}</b></p>
</div>
<div className={styles.fold} aria-hidden={same} inert={same ? "" : undefined}>
<div className={styles.inner}>
<p className={styles.field}>
<span className={styles.fk}>{nameLabel}</span>
<input
className={styles.fv}
type="text"
value={shipName}
aria-label={nameLabel}
onChange={(e: FieldChange) => setShipName(e.target.value)}
/>
</p>
<p className={styles.field}>
<span className={styles.fk}>{addressLabel}</span>
<input
className={styles.fv}
type="text"
value={shipAddress}
aria-label={addressLabel}
onChange={(e: FieldChange) => setShipAddress(e.target.value)}
/>
</p>
<p className={styles.field}>
<span className={styles.fk}>{telLabel}</span>
<input
className={styles.fv}
type="tel"
value={shipTel}
aria-label={telLabel}
onChange={(e: FieldChange) => setShipTel(e.target.value)}
/>
</p>
</div>
</div>
</section>
);
}06 Shipping fields that fold away and fill themselves in — Measured area 22.444% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · Checkout Page Design — 9 Cart Patterns, No Library
The post this demo lives in
Checkout Page Design — 9 Cart Patterns, No LibraryCheckout page design covers every screen between "I want this" and the pay button. These nine patterns are plain HTML, S See all demos in this post
If you like this effect - similar demos
Inputs06 Vertical wizard that folds finished steps into one line9 Multi Step Form UI Patterns — Branch and Resume
Inputs08 Eyedropper — a color picker that recomputes its preview from the extracted color's brightness9 Color Picker Design Patterns (Copy-Paste)
Inputs08 Fields that shake when submit catches them9 Form Validation UI Patterns — As You Type