05고른 결제 수단만 아래로 펼쳐지는 카드
결제 수단 카드 세 장 중 하나를 고르면 그 카드 아래 상세 칸만 높이 0에서 펴지고, 앞서 열려 있던 칸은 같은 시간 동안 닫힙니다.
쓰임카드·간편결제·계좌이체를 고르는 결제 화면
grid-template-rowsaria-expanded:checked

<main class="stage">
<form class="pm is-demo" id="pm">
<fieldset class="pm__set">
<legend class="pm__legend">결제 수단</legend>
<div class="pm__item">
<label class="pm__row" aria-expanded="true">
<input class="pm__radio" type="radio" name="payMethod" value="card" checked>
<span class="pm__ic" aria-hidden="true"><svg viewBox="0 0 256 256" fill="currentColor"><path d="M232,96v96a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V96Z" opacity="0.2"/><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,16V88H32V64Zm0,128H32V104H224v88Zm-16-24a8,8,0,0,1-8,8H168a8,8,0,0,1,0-16h32A8,8,0,0,1,208,168Zm-64,0a8,8,0,0,1-8,8H120a8,8,0,0,1,0-16h16A8,8,0,0,1,144,168Z"/></svg></span>
<span class="pm__name">카드</span>
<span class="pm__dot" aria-hidden="true"></span>
</label>
<div class="pm__fold">
<p class="pm__detail"><span class="pm__dtext">카드 번호와 할부 개월을 여기서 고릅니다</span></p>
</div>
</div>
<div class="pm__item">
<label class="pm__row" aria-expanded="false">
<input class="pm__radio" type="radio" name="payMethod" value="wallet">
<span class="pm__ic" aria-hidden="true"><svg viewBox="0 0 256 256" fill="currentColor"><path d="M224,80V192a8,8,0,0,1-8,8H56a16,16,0,0,1-16-16V56A16,16,0,0,0,56,72H216A8,8,0,0,1,224,80Z" opacity="0.2"/><path d="M216,64H56a8,8,0,0,1,0-16H192a8,8,0,0,0,0-16H56A24,24,0,0,0,32,56V184a24,24,0,0,0,24,24H216a16,16,0,0,0,16-16V80A16,16,0,0,0,216,64Zm0,128H56a8,8,0,0,1-8-8V78.63A23.84,23.84,0,0,0,56,80H216Zm-48-60a12,12,0,1,1,12,12A12,12,0,0,1,168,132Z"/></svg></span>
<span class="pm__name">간편결제</span>
<span class="pm__dot" aria-hidden="true"></span>
</label>
<div class="pm__fold">
<p class="pm__detail"><span class="pm__dtext">앱에서 지문으로 확인하면 끝납니다</span></p>
</div>
</div>
<div class="pm__item">
<label class="pm__row" aria-expanded="false">
<input class="pm__radio" type="radio" name="payMethod" value="bank">
<span class="pm__ic" aria-hidden="true"><svg viewBox="0 0 256 256" fill="currentColor"><path d="M232,96H24L128,32Z" opacity="0.2"/><path d="M24,104H48v64H32a8,8,0,0,0,0,16H224a8,8,0,0,0,0-16H208V104h24a8,8,0,0,0,4.19-14.81l-104-64a8,8,0,0,0-8.38,0l-104,64A8,8,0,0,0,24,104Zm40,0H96v64H64Zm80,0v64H112V104Zm48,64H160V104h32ZM128,41.39,203.74,88H52.26ZM248,208a8,8,0,0,1-8,8H16a8,8,0,0,1,0-16H240A8,8,0,0,1,248,208Z"/></svg></span>
<span class="pm__name">계좌이체</span>
<span class="pm__dot" aria-hidden="true"></span>
</label>
<div class="pm__fold">
<p class="pm__detail"><span class="pm__dtext">입금하실 계좌를 다음 화면에 띄웁니다</span></p>
</div>
</div>
</fieldset>
</form>
</main>@use "../../../../site/static/demo-tokens" as *;
// 고른 결제 수단만 아래로 펼쳐지는 카드 — 상세 칸은 grid-template-rows 0fr→1fr 로 문서 흐름 안에서 편다.
// 절대배치 패널로 띄우면 320px 에서 실제 높이가 화면을 넘긴다(BD9)
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.pm {
box-sizing: border-box;
width: min(300px, 92vw);
color: $subject-cream;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.pm * { box-sizing: border-box; }
.pm__set {
display: grid;
gap: $sp-1;
margin: 0;
padding: 0;
border: 0;
}
.pm__legend {
padding: 0;
margin-bottom: $sp-2;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
letter-spacing: .04em;
color: rgba($subject-cream, .62);
}
.pm__item {
position: relative;
border-radius: $r-control;
background: rgba($subject-cream, .08);
}
// 고른 표시 — 테두리·바탕을 따로 그린 겹으로 두고 opacity 만 켠다.
// 그래야 미리보기 루프에서 컷으로 갈아 끼워도 두 카드가 반투명하게 겹치지 않는다
.pm__item::before {
content: "";
position: absolute;
inset: 0;
border: 2px solid $color;
border-radius: $r-control;
background: rgba($subject-blue, .18);
opacity: 0;
pointer-events: none;
transition: opacity $dur-quick linear;
}
.pm__item:has(.pm__radio:checked)::before { opacity: 1; }
.pm__row {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-2;
cursor: pointer;
}
.pm__radio {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
.pm__ic {
flex: 0 0 auto;
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
color: rgba($subject-cream, .72);
}
.pm__ic svg { width: 18px; height: 18px; }
.pm__item:has(.pm__radio:checked) .pm__ic { color: $subject-cream; }
.pm__name {
flex: 1 1 auto;
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
.pm__dot {
position: relative;
flex: 0 0 auto;
width: 16px;
height: 16px;
border: 2px solid rgba($subject-cream, .45);
border-radius: 50%;
}
.pm__dot::after {
content: "";
position: absolute;
inset: 2px;
border-radius: 50%;
background: $color;
opacity: 0;
transform: scale(.4);
transition: opacity $dur-instant linear, transform $dur-quick $easing;
}
.pm__item:has(.pm__radio:checked) .pm__dot { border-color: $color; }
.pm__item:has(.pm__radio:checked) .pm__dot::after { opacity: 1; transform: scale(1); }
// 키보드로 옮겨 다닐 때도 어느 카드에 서 있는지 보인다
.pm__item:has(.pm__radio:focus-visible) { outline: 2px solid $subject-cream; outline-offset: 2px; }
// 상세 칸 — 닫힘 0fr, 열림 1fr. 높이를 px 로 짐작하지 않으니 글자가 길어져도 맞는다
.pm__fold {
position: relative;
z-index: 1;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $duration $easing;
}
.pm__item:has(.pm__radio:checked) .pm__fold { grid-template-rows: 1fr; }
.pm__detail {
min-height: 0;
margin: 0;
overflow: hidden;
}
.pm__dtext {
display: block;
padding: 0 $sp-2 $sp-2;
font-size: 11px;
line-height: 1.4;
color: rgba($subject-cream, .78);
}
// ── 격자 미리보기 루프: 1번 열림 → 2번 열림 → 1번 복귀. 상태 교체는 컷, 곁들이는 움직임은 transform ──
@keyframes pm-fold-1 {
0%, 42% { grid-template-rows: 1fr; }
46%, 84% { grid-template-rows: 0fr; }
88%, 100% { grid-template-rows: 1fr; }
}
@keyframes pm-fold-2 {
0%, 42% { grid-template-rows: 0fr; }
46%, 84% { grid-template-rows: 1fr; }
88%, 100% { grid-template-rows: 0fr; }
}
@keyframes pm-on-1 {
0%, 42% { opacity: 1; }
46%, 84% { opacity: 0; }
88%, 100% { opacity: 1; }
}
@keyframes pm-on-2 {
0%, 42% { opacity: 0; }
46%, 84% { opacity: 1; }
88%, 100% { opacity: 0; }
}
@keyframes pm-dot-1 {
0%, 42% { opacity: 1; transform: scale(1); }
46%, 84% { opacity: 0; transform: scale(.4); }
88%, 100% { opacity: 1; transform: scale(1); }
}
@keyframes pm-dot-2 {
0%, 42% { opacity: 0; transform: scale(.4); }
46%, 84% { opacity: 1; transform: scale(1); }
88%, 100% { opacity: 0; transform: scale(.4); }
}
// 컷만으로는 움직인 프레임이 두어 장뿐이라 측정이 안 찬다 — 펼쳐진 글이 한 번 올라서며 프레임을 채운다.
// 투명도가 아니라 transform 이라 중간 프레임이 유령이 되지 않는다
@keyframes pm-rise-1 {
0%, 87% { transform: translateY(0); }
88% { transform: translateY(10px); }
100% { transform: translateY(0); }
}
@keyframes pm-rise-2 {
0%, 45% { transform: translateY(0); }
46% { transform: translateY(10px); }
62%, 100% { transform: translateY(0); }
}
@keyframes pm-pop-1 {
0%, 87% { transform: scale(1); }
88% { transform: scale(1.35); }
100% { transform: scale(1); }
}
@keyframes pm-pop-2 {
0%, 45% { transform: scale(1); }
46% { transform: scale(1.35); }
62%, 100% { transform: scale(1); }
}
.pm.is-demo {
.pm__fold,
.pm__item::before,
.pm__dot::after {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.pm__dtext,
.pm__ic {
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.pm__item:nth-child(2) {
.pm__fold { animation-name: pm-fold-1; }
&::before { animation-name: pm-on-1; }
.pm__dot::after { animation-name: pm-dot-1; }
.pm__dtext { animation-name: pm-rise-1; }
.pm__ic { animation-name: pm-pop-1; }
}
.pm__item:nth-child(3) {
.pm__fold { animation-name: pm-fold-2; }
&::before { animation-name: pm-on-2; }
.pm__dot::after { animation-name: pm-dot-2; }
.pm__dtext { animation-name: pm-rise-2; }
.pm__ic { animation-name: pm-pop-2; }
}
}
@media (max-width: 340px) {
.pm__legend { margin-bottom: $sp-1; font-size: 10px; }
.pm__row { padding: $sp-1 $sp-2; }
.pm__name { font-size: 11px; }
.pm__dtext { padding: 0 $sp-2 $sp-1; font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
// 정지 상태에서도 「1번이 골라져 펼쳐져 있다」 는 것은 보인다 — 움직임만 뺀다
.pm.is-demo .pm__fold,
.pm.is-demo .pm__item::before,
.pm.is-demo .pm__dot::after,
.pm.is-demo .pm__dtext,
.pm.is-demo .pm__ic { animation: none !important; }
.pm.is-demo .pm__dtext,
.pm.is-demo .pm__ic { transform: none !important; }
.pm__fold { transition: none !important; }
.pm__item::before { transition: none !important; }
.pm__dot::after { 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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pm {
box-sizing: border-box;
width: min(300px, 92vw);
color: #fff7e6;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.pm * {
box-sizing: border-box;
}
.pm__set {
display: grid;
gap: 4px;
margin: 0;
padding: 0;
border: 0;
}
.pm__legend {
padding: 0;
margin-bottom: 8px;
font-size: 11px;
font-weight: 700;
line-height: 1.3;
letter-spacing: 0.04em;
color: rgba(255, 247, 230, 0.62);
}
.pm__item {
position: relative;
border-radius: 12px;
background: rgba(255, 247, 230, 0.08);
}
.pm__item::before {
content: "";
position: absolute;
inset: 0;
border: 2px solid #2f6df6;
border-radius: 12px;
background: rgba(47, 109, 246, 0.18);
opacity: 0;
pointer-events: none;
transition: opacity 220ms linear;
}
.pm__item:has(.pm__radio:checked)::before {
opacity: 1;
}
.pm__row {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
cursor: pointer;
}
.pm__radio {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
.pm__ic {
flex: 0 0 auto;
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
color: rgba(255, 247, 230, 0.72);
}
.pm__ic svg {
width: 18px;
height: 18px;
}
.pm__item:has(.pm__radio:checked) .pm__ic {
color: #fff7e6;
}
.pm__name {
flex: 1 1 auto;
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
.pm__dot {
position: relative;
flex: 0 0 auto;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 247, 230, 0.45);
border-radius: 50%;
}
.pm__dot::after {
content: "";
position: absolute;
inset: 2px;
border-radius: 50%;
background: #2f6df6;
opacity: 0;
transform: scale(0.4);
transition: opacity 140ms linear, transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pm__item:has(.pm__radio:checked) .pm__dot {
border-color: #2f6df6;
}
.pm__item:has(.pm__radio:checked) .pm__dot::after {
opacity: 1;
transform: scale(1);
}
.pm__item:has(.pm__radio:focus-visible) {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.pm__fold {
position: relative;
z-index: 1;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pm__item:has(.pm__radio:checked) .pm__fold {
grid-template-rows: 1fr;
}
.pm__detail {
min-height: 0;
margin: 0;
overflow: hidden;
}
.pm__dtext {
display: block;
padding: 0 8px 8px;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, 0.78);
}
@keyframes pm-fold-1 {
0%, 42% {
grid-template-rows: 1fr;
}
46%, 84% {
grid-template-rows: 0fr;
}
88%, 100% {
grid-template-rows: 1fr;
}
}
@keyframes pm-fold-2 {
0%, 42% {
grid-template-rows: 0fr;
}
46%, 84% {
grid-template-rows: 1fr;
}
88%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes pm-on-1 {
0%, 42% {
opacity: 1;
}
46%, 84% {
opacity: 0;
}
88%, 100% {
opacity: 1;
}
}
@keyframes pm-on-2 {
0%, 42% {
opacity: 0;
}
46%, 84% {
opacity: 1;
}
88%, 100% {
opacity: 0;
}
}
@keyframes pm-dot-1 {
0%, 42% {
opacity: 1;
transform: scale(1);
}
46%, 84% {
opacity: 0;
transform: scale(0.4);
}
88%, 100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes pm-dot-2 {
0%, 42% {
opacity: 0;
transform: scale(0.4);
}
46%, 84% {
opacity: 1;
transform: scale(1);
}
88%, 100% {
opacity: 0;
transform: scale(0.4);
}
}
@keyframes pm-rise-1 {
0%, 87% {
transform: translateY(0);
}
88% {
transform: translateY(10px);
}
100% {
transform: translateY(0);
}
}
@keyframes pm-rise-2 {
0%, 45% {
transform: translateY(0);
}
46% {
transform: translateY(10px);
}
62%, 100% {
transform: translateY(0);
}
}
@keyframes pm-pop-1 {
0%, 87% {
transform: scale(1);
}
88% {
transform: scale(1.35);
}
100% {
transform: scale(1);
}
}
@keyframes pm-pop-2 {
0%, 45% {
transform: scale(1);
}
46% {
transform: scale(1.35);
}
62%, 100% {
transform: scale(1);
}
}
.pm.is-demo .pm__fold,
.pm.is-demo .pm__item::before,
.pm.is-demo .pm__dot::after {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.pm.is-demo .pm__dtext,
.pm.is-demo .pm__ic {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.pm.is-demo .pm__item:nth-child(2) .pm__fold {
animation-name: pm-fold-1;
}
.pm.is-demo .pm__item:nth-child(2)::before {
animation-name: pm-on-1;
}
.pm.is-demo .pm__item:nth-child(2) .pm__dot::after {
animation-name: pm-dot-1;
}
.pm.is-demo .pm__item:nth-child(2) .pm__dtext {
animation-name: pm-rise-1;
}
.pm.is-demo .pm__item:nth-child(2) .pm__ic {
animation-name: pm-pop-1;
}
.pm.is-demo .pm__item:nth-child(3) .pm__fold {
animation-name: pm-fold-2;
}
.pm.is-demo .pm__item:nth-child(3)::before {
animation-name: pm-on-2;
}
.pm.is-demo .pm__item:nth-child(3) .pm__dot::after {
animation-name: pm-dot-2;
}
.pm.is-demo .pm__item:nth-child(3) .pm__dtext {
animation-name: pm-rise-2;
}
.pm.is-demo .pm__item:nth-child(3) .pm__ic {
animation-name: pm-pop-2;
}
@media (max-width: 340px) {
.pm__legend {
margin-bottom: 4px;
font-size: 10px;
}
.pm__row {
padding: 4px 8px;
}
.pm__name {
font-size: 11px;
}
.pm__dtext {
padding: 0 8px 4px;
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.pm.is-demo .pm__fold,
.pm.is-demo .pm__item::before,
.pm.is-demo .pm__dot::after,
.pm.is-demo .pm__dtext,
.pm.is-demo .pm__ic {
animation: none !important;
}
.pm.is-demo .pm__dtext,
.pm.is-demo .pm__ic {
transform: none !important;
}
.pm__fold {
transition: none !important;
}
.pm__item::before {
transition: none !important;
}
.pm__dot::after {
transition: none !important;
}
}// 고른 수단의 상세 칸만 편다 — 여닫는 힘은 :checked 가 내고, JS 는 aria-expanded 만 맞춘다
var pm = document.getElementById('pm');
var rows = pm.querySelectorAll('.pm__row');
function syncExpanded() {
for (var i = 0; i < rows.length; i++) {
var picked = rows[i].querySelector('.pm__radio').checked;
rows[i].setAttribute('aria-expanded', picked ? 'true' : 'false');
}
}
// 라디오는 키보드 화살표로도 옮겨 다닌다 — change 하나면 마우스·키보드가 같이 걸린다
pm.addEventListener('change', function () {
pm.classList.remove('is-demo');
syncExpanded();
});
syncExpanded();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./PayMethodCardExpand.module.scss";
export type PayMethodCardExpandProps = {
legend?: string;
cardName?: string;
cardDetail?: string;
walletName?: string;
walletDetail?: string;
bankName?: string;
bankDetail?: string;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
// ambient 가 얇아 이벤트 타입을 못 쓴다 — 필요한 꼴만 로컬 타입으로 선언
type Method = { value: string; name: string; detail: string; icon: ReactNode };
function CardIcon() {
return (
<svg viewBox="0 0 256 256" fill="currentColor"><path d="M232,96v96a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V96Z" opacity="0.2"/><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,16V88H32V64Zm0,128H32V104H224v88Zm-16-24a8,8,0,0,1-8,8H168a8,8,0,0,1,0-16h32A8,8,0,0,1,208,168Zm-64,0a8,8,0,0,1-8,8H120a8,8,0,0,1,0-16h16A8,8,0,0,1,144,168Z"/></svg>
);
}
function WalletIcon() {
return (
<svg viewBox="0 0 256 256" fill="currentColor"><path d="M224,80V192a8,8,0,0,1-8,8H56a16,16,0,0,1-16-16V56A16,16,0,0,0,56,72H216A8,8,0,0,1,224,80Z" opacity="0.2"/><path d="M216,64H56a8,8,0,0,1,0-16H192a8,8,0,0,0,0-16H56A24,24,0,0,0,32,56V184a24,24,0,0,0,24,24H216a16,16,0,0,0,16-16V80A16,16,0,0,0,216,64Zm0,128H56a8,8,0,0,1-8-8V78.63A23.84,23.84,0,0,0,56,80H216Zm-48-60a12,12,0,1,1,12,12A12,12,0,0,1,168,132Z"/></svg>
);
}
function BankIcon() {
return (
<svg viewBox="0 0 256 256" fill="currentColor"><path d="M232,96H24L128,32Z" opacity="0.2"/><path d="M24,104H48v64H32a8,8,0,0,0,0,16H224a8,8,0,0,0,0-16H208V104h24a8,8,0,0,0,4.19-14.81l-104-64a8,8,0,0,0-8.38,0l-104,64A8,8,0,0,0,24,104Zm40,0H96v64H64Zm80,0v64H112V104Zm48,64H160V104h32ZM128,41.39,203.74,88H52.26ZM248,208a8,8,0,0,1-8,8H16a8,8,0,0,1,0-16H240A8,8,0,0,1,248,208Z"/></svg>
);
}
export default function PayMethodCardExpand({
legend = "결제 수단",
cardName = "카드",
cardDetail = "카드 번호와 할부 개월을 여기서 고릅니다",
walletName = "간편결제",
walletDetail = "앱에서 지문으로 확인하면 끝납니다",
bankName = "계좌이체",
bankDetail = "입금하실 계좌를 다음 화면에 띄웁니다",
demo = true,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: PayMethodCardExpandProps) {
const [picked, setPicked] = useState("card");
const [live, setLive] = useState(demo);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const methods: Method[] = [
{ value: "card", name: cardName, detail: cardDetail, icon: <CardIcon /> },
{ value: "wallet", name: walletName, detail: walletDetail, icon: <WalletIcon /> },
{ value: "bank", name: bankName, detail: bankDetail, icon: <BankIcon /> },
];
// 라디오는 키보드 화살표로도 옮겨 다닌다 — onChange 하나면 마우스·키보드가 같이 걸린다
function pick(value: string) {
setLive(false);
setPicked(value);
}
const loopClass = [styles.loopA, styles.loopB, ""];
return (
<form className={[styles.pm, live ? styles.isDemo : ""].filter(Boolean).join(" ")} style={vars}>
<fieldset className={styles.set}>
<legend className={styles.legend}>{legend}</legend>
{methods.map((m, i) => (
<div key={m.value} className={[styles.item, loopClass[i]].filter(Boolean).join(" ")}>
<label className={styles.row} aria-expanded={picked === m.value}>
<input
className={styles.radio}
type="radio"
name="payMethod"
value={m.value}
checked={picked === m.value}
onChange={() => pick(m.value)}
/>
<span className={styles.ic} aria-hidden="true">{m.icon}</span>
<span className={styles.name}>{m.name}</span>
<span className={styles.dot} aria-hidden="true"></span>
</label>
{/* 상세 칸 — 닫힘 0fr, 열림 1fr. 높이를 px 로 짐작하지 않으니 글자가 길어져도 맞는다 */}
<div className={styles.fold}>
<p className={styles.detail}><span className={styles.dtext}>{m.detail}</span></p>
</div>
</div>
))}
</fieldset>
</form>
);
}05 고른 결제 수단만 아래로 펼쳐지는 카드 — 측정 누적면적 17.7% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 장바구니 UI 9종 — 복붙하면 되는 수량·쿠폰·결제 코드
이 데모가 속한 글
장바구니 UI 9종 — 복붙하면 되는 수량·쿠폰·결제 코드장바구니 UI 는 손님이 물건을 담은 뒤 결제 버튼을 누르기 직전까지 마주치는 화면 묶음입니다. 수량을 바꾸면 합계가 따라오는 줄, 쿠폰을 넣으면 끼어드는 할인 줄, 결제 수단을 고르면 펼쳐지는 카드 같은 것들인데, 이 글의 데모 전부 보기


