Footer | CoUI
본문으로 건너뛰기
LogoCoUI

Footer

페이지 하단의 사이트 푸터 (내비게이션 섹션 + 법적 고지 블록)

목차

페이지 하단에 최대 세 블록을 세로로 쌓는 푸터입니다. 옵션 상단 구분선(showDivider), 브랜딩(aside) + 내비게이션 섹션으로 이루어진 밴드, 그리고 법적 고지 블록(회사명 · 사업자 정보 디스클로저 · 링크 줄 · 저작권)입니다. 슬롯이 빈 블록은 통째로 빠지므로, 내비게이션만 넘긴 푸터는 법적 고지 블록이 없던 때와 똑같이 그려집니다.

클래스 이름은 그대로입니다. Footer, CoreFooterContract, CoreFooterLayout, CoreFooterStyle.

Live Preview#

개요#

이 문서는 /components/footer 에 있었습니다. 지금은 /blocks/footer 입니다. 같은 위젯이고, Dart 와 Web 의 API 이름은 바뀌지 않았습니다.

옛 주소로 들어오면 /blocks/footer 로 옮깁니다.

구조#

푸터가 직접 그리는 크롬은 배경과 패딩뿐입니다. 글자, 링크, 간격, 구분선, 디스클로저는 아래 컴포넌트를 합성합니다.

자리컴포넌트
섹션 제목 · 본문 · aside · 회사명 · 저작권Text (호출자가 넣는 위젯)
법적 링크 줄Link
섹션 제목과 본문 사이Gap (sectionTitleGapStyle)
상단 규칙 · 행 안 눈금Divider
사업자 정보Collapsible

aside 는 섹션보다 앞입니다. 로고나 짧은 카피를 둡니다. Figma 에서는 Aside 인스턴스 스왑과 Show aside 가 이 슬롯입니다.

sections 는 CoreFooterNavData 의 목록입니다. 각 항목은 선택적 title 과 필수 children 입니다.

사용 시기 (When to Use)#

페이지 끝에서 링크 섹션·회사 정보·저작권을 함께 묶을 때 씁니다. 선택된 목적지를 표시하고 이동 이벤트를 처리해야 하면 NavigationBar를, 사업자 정보만 접었다 펼쳐야 하면 Collapsible을 사용합니다. Footer는 페이지 흐름의 콘텐츠이며 고정 내비게이션이나 오버레이를 만들지 않습니다.

레이아웃#

축은 Layout=horizontal | vertical | center 하나뿐입니다. 멤버는 CoreFooterLayout 과 같습니다.

값그리는 것
horizontal (기본)aside 와 섹션이 한 줄에서 감쌉니다. 좁으면 다음 줄로 내려갑니다.
vertical위→아래. 가로 축은 앞쪽(leading)입니다.
center위→아래. 가로 축과 주변 텍스트를 가운데 정렬합니다.

layout 은 내비게이션 밴드만 바꿉니다. 상단 구분선과 법적 고지 블록은 언제나 밴드 위와 아래에 세로로 쌓입니다.

위젯의 layout 이 null 이면 스타일 체인의 layout, 그다음 CoreFooterStyle.defaultLayout (horizontal) 입니다. 테마는 CoreFooterTheme.style 만 덮고, 시맨틱 layout 을 따로 이기지 않습니다.

Figma 의 Info 는 이 축이 아닙니다. 사업자 정보 디스클로저가 펼쳐졌는지(businessInfoExpanded)입니다.

기본 사용법 (Basic Usage)#

기본 프리뷰는 horizontal 입니다. vertical 과 center 는 같은 슬롯을 그 축으로 다시 놓습니다. Widgetbook 의 $Default, $Vertical, $Center 와 같은 세 레이아웃입니다.

Footer(
  aside: const Text('© 2026 CoUI — Industrial Ltd.'),
  sections: const [
    CoreFooterNavData<Widget>(
      title: 'Services',
      children: [
        Text('Branding'),
        Text('Design'),
        Text('Marketing'),
      ],
    ),
    CoreFooterNavData<Widget>(
      title: 'Company',
      children: [
        Text('About us'),
        Text('Contact'),
        Text('Jobs'),
      ],
    ),
  ],
)

빠른 오버라이드 (Chain)#

이미 만든 Footer 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .surfaceContainer처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(surfaceContainer == CoreColors.surfaceContainer) 어느 컴포넌트에서 써도 뜻이 갈리지 않으며, 위 예시처럼 withStyle 뒤에 이어붙일 수도 있습니다.

class FooterChainExample extends StatelessWidget {
  const FooterChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Footer(
          aside: const Text('© 2026 CoUI — Industrial Ltd.'),
          sections: const [
            CoreFooterNavData<Widget>(
              title: 'Services',
              children: [
                Text('Branding'),
                Text('Design'),
                Text('Marketing'),
              ],
            ),
            CoreFooterNavData<Widget>(
              title: 'Company',
              children: [
                Text('About us'),
                Text('Contact'),
                Text('Jobs'),
              ],
            ),
            CoreFooterNavData<Widget>(
              title: 'Legal',
              children: [
                Text('Terms'),
                Text('Privacy'),
                Text('Cookies'),
              ],
            ),
          ],
        )
        .withStyle(
          const CoreFooterStyle(
            padding: CoreEdgeInsets.all(CoreSpace.space48),
            sectionSpacing: CoreSpace.space48,
            sectionTitleGapStyle: CoreGapStyle(size: CoreSpace.space12),
          ),
        )
        .surfaceContainer;
  }
}
class FooterChainExample extends StatelessComponent {
  const FooterChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return Footer(
          aside: div([Text('© 2026 CoUI — Industrial Ltd.')]),
          sections: [
            CoreFooterNavData<Component>(
              title: 'Services',
              children: [
                div([Text('Branding')]),
                div([Text('Design')]),
                div([Text('Marketing')]),
              ],
            ),
            CoreFooterNavData<Component>(
              title: 'Company',
              children: [
                div([Text('About us')]),
                div([Text('Contact')]),
                div([Text('Jobs')]),
              ],
            ),
            CoreFooterNavData<Component>(
              title: 'Legal',
              children: [
                div([Text('Terms')]),
                div([Text('Privacy')]),
                div([Text('Cookies')]),
              ],
            ),
          ],
        )
        .withStyle(
          const CoreFooterStyle(
            padding: CoreEdgeInsets.all(CoreSpace.space48),
            sectionSpacing: CoreSpace.space48,
            sectionTitleGapStyle: CoreGapStyle(size: CoreSpace.space12),
          ),
        )
        .surfaceContainer;
  }
}

레시피#

법적 고지와 상단 구분선은 별도 레이아웃이 아닙니다. 같은 Footer 의 슬롯입니다.

법적 고지#

스토어프론트가 공시하는 회사명, 사업자 정보, 약관 링크, 저작권입니다. 네 슬롯이 전부 비면 블록 자체가 그려지지 않습니다.

Footer(
  company: const Text('Cocode Inc.'),
  businessInfoLabel: const Text('Business info'),
  businessInfo: const Text('CEO Jane Doe · Business registration no. 123-45-67890'),
  links: const [
    Link(href: '/refund', child: Text('Refund policy')),
    Link(href: '/terms', child: Text('Terms of service')),
    Link(href: '/privacy', child: Text('Privacy policy')),
  ],
  copyright: const Text('© 2026 Cocode Inc. All rights reserved.'),
  sections: const [],
)
  • 디스클로저는 Collapsible 입니다. 푸터는 businessInfoLabel 과 businessInfo 만 넘깁니다.
  • businessInfo 가 null 이면 트리거도 그리지 않습니다. 열 문단이 없으면 트리거가 남지 않습니다.
  • 회사명과 디스클로저 사이, 링크와 링크 사이의 눈금은 세로 Divider 입니다. 색과 두께는 inlineDividerStyle, 길이는 inlineDividerLength 입니다.
  • links 의 타입은 List<W> 입니다. 눈금은 항목 사이에만 들어가므로 항목이 하나면 눈금은 없습니다.

상단 구분선#

showDivider 의 기본은 꺼짐입니다 (CoreFooterContract.defaultShowDivider). 아무 말도 하지 않은 푸터는 선을 그리지 않습니다.

Footer(showDivider: true, sections: const [])

이 선은 full-bleed 입니다. 밴드의 가로 패딩 밖, 푸터의 양 끝까지입니다. 색과 두께는 dividerStyle 이고, 비워두면 Divider 의 기본값입니다.

이 블록에 없는 것#

메가 푸터, 뉴스레터, 거대 텍스트, 배경 이미지는 새 프로퍼티가 아닙니다. 위의 레이아웃과 슬롯으로 조합하거나, 나중에 별도 레시피로 둡니다. Ecommerce Footer 는 이 블록이 아닙니다.

Props / Parameters#

W는 합성 콘텐츠 타입입니다. Flutter에서는 Widget, Web에서는 Component이며, CoreFooterNavData<W>와 슬롯에 같은 플랫폼 타입을 사용합니다.

속성타입기본값설명
sections List<CoreFooterNavData<W>> 필수 렌더링할 nav 섹션 목록
asideW?null브랜딩/로고 슬롯 (sections 앞에 배치)
layout CoreFooterLayout? horizontal horizontal / vertical / center. null 이면 스타일 체인, 그다음 기본값
company W? null 법적 고지 블록 첫 줄의 회사명. 주변 텍스트 스타일은 companyTextStyle 이 심으므로 평범한 Text('…') 가 그대로 상속받습니다
businessInfoLabel W? null 사업자 정보 디스클로저의 트리거 라벨. 셰브론은 합성된 Collapsible 이 직접 그리므로 이 슬롯은 라벨만 담습니다
businessInfo W? null 디스클로저가 여는 사업자 정보 문단. null 이면 디스클로저 자체가 없습니다 — 트리거도 셰브론도 앞의 구분 눈금도 그리지 않습니다
businessInfoExpanded bool false 디스클로저의 초기 펼침 상태. onBusinessInfoExpandedChanged 를 주지 않으면 Collapsible 이 이 값에서 시작해 스스로 토글합니다
onBusinessInfoExpandedChanged void Function(bool)? null 디스클로저 토글 콜백. 넘기면 businessInfoExpanded 는 호출자가 소유하는 controlled 값이 됩니다
links List<W> const [] 법적 링크 줄. 항목 사이에 hairline 눈금(Divider)이 들어갑니다. 비면 줄 자체를 그리지 않습니다
copyright W? null 법적 고지 블록을 닫는 저작권 줄. 주변 텍스트 스타일은 copyrightTextStyle 이 심습니다
showDivider bool false 내비게이션 밴드 위 full-bleed 상단 구분선. 기본 꺼짐 옵션
semanticLabel String? null 랜드마크 이름. 생략하면 로케일 폴백(footerLabel)
footerStyle CoreFooterStyle? null 모든 chrome 이 지나는 단일 슬롯 (아래 표 참고)

CoreFooterNavData<W>#

속성타입기본값설명
title String? null 섹션 제목 (titleSmall, 역할의 굵기). 없거나 빈 문자열이면 생략
childrenList<W>필수섹션 안의 링크/노드 목록

스타일 시스템 (Style System)#

CoreFooterStyle 필드#

필드타입설명
layoutCoreFooterLayout?Layout mode override.
padding CoreEdgeInsets? Padding of the content box below the opt-in top rule (pre-scaling).
blockSpacing double? Spacing between the blocks inside the padded content box — navigation band and legal block (logical px).
sectionSpacing double? Spacing between top-level sections (logical px).
legalSpacing double? Spacing between the legal block's rows (logical px).
companyRowSpacing double? Spacing inside the company row — name, tick, disclosure trigger (logical px).
linkRowSpacing double? Spacing inside the legal link row — links and the ticks between them (logical px).
inlineDividerLength double? Length of the hairline tick separating inline legal items (logical px, pre-scaling). Sizes the box the composed Divider draws into — the divider itself has no length.
sectionTitleGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between a nav section title and its children. Forwarded to the Gap widget rendered between the title and the section body on Flutter, and drives the title <div> 's inline margin-bottom (via size ) on Web. Merged on top of [defaultSectionTitleGapStyle].
backgroundColor CoreColor? Default background colour.
titleTextStyle CoreTextStyle? Nav section title text style override. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw textColor field removed and folded into both this and [bodyTextStyle]).
bodyTextStyle CoreTextStyle? Body / link text style override. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8).
companyTextStyle CoreTextStyle? Company-name text style override, merged on top of [defaultCompanyTextStyle] and installed as the ambient text style around the company slot. Honoured in full — role and colour.
copyrightTextStyle CoreTextStyle? Copyright-line text style override, merged on top of [defaultCopyrightTextStyle] and installed as the ambient text style around the copyright slot. Honoured in full — role and colour.
dividerStyle CoreDividerStyle? Nested [CoreDividerStyle] slot for the composed top rule. Merged on top of [defaultDividerStyle] and raw-forwarded — Divider 's own resolver fills the remaining defaults.
inlineDividerStyle CoreDividerStyle? Nested [CoreDividerStyle] slot for the composed hairline ticks between inline legal items. Merged on top of [defaultInlineDividerStyle] and raw-forwarded. The tick's length lives in [inlineDividerLength], not here.
collapsibleStyle CoreCollapsibleStyle? Nested [CoreCollapsibleStyle] slot for the composed business-info disclosure — trigger typography, label ↔ chevron gap, chevron icon, expand duration. Merged on top of [defaultCollapsibleStyle] and raw-forwarded; the Collapsible 's own resolver fills the rest.

Resolve chain#

모든 chrome 은 footerStyle 하나로 들어가고, 아래 순서로 합쳐집니다 — 뒤에 오는 쪽이 이깁니다.

CoreFooterStyle.defaultX      // 디자인 시스템 기본값
  → CoreFooterTheme.style     // 프로젝트 공통
  → 부모 컴포넌트의 슬롯 override
  → widget.footerStyle        // 인스턴스별

dividerStyle / inlineDividerStyle / collapsibleStyle / sectionTitleGapStyle 은 중첩 슬롯이라 값을 여기서 풀지 않고 그대로 자식 컴포넌트에 넘깁니다 — 각 자식(Divider / Collapsible / Gap)의 resolver 가 자기 기본값을 채웁니다. 그래서 이 슬롯들을 비워두면 자식의 기본값이 그대로 쓰이고, 일부 필드만 채우면 나머지는 자식 기본값이 남습니다.

동작 스펙 (Behavior)#

  • aside와 sections가 모두 비면 내비게이션 밴드를 생략합니다. company·businessInfo·links·copyright가 모두 비면 법적 고지 블록도 생략하며 빈 블록 사이 간격을 남기지 않습니다.
  • layout은 내비게이션 배치와 정렬을 선택합니다. horizontal은 감싸는 행, vertical과 center는 세로 배치이며, 상단 구분선과 법적 고지 블록은 세로 흐름에 남습니다.
  • businessInfo가 있을 때만 Collapsible 트리거를 만듭니다. 콜백이 없으면 초기 businessInfoExpanded에서 내부 토글하고, onBusinessInfoExpandedChanged가 있으면 새 값을 알린 뒤 호출자가 상태를 다시 전달해야 합니다.
  • showDivider가 켜져야 상단 구분선이 보이며, 법적 링크 사이 눈금은 두 항목 사이에만 삽입합니다. 링크 이동과 클릭 이벤트는 슬롯에 넣은 자식이 담당합니다.

사용 가이드라인 (Usage Guidelines)#

✅ Do#

콘텐츠 밀도에 맞는 layout 선택

Footer(
  aside: const Text('© 2026 CoUI'),
  sections: const [],
  layout: CoreFooterLayout.center,
)

섹션 없이 카피라이트만 있는 얇은 푸터에는 center(세로 stack + 중앙 정렬)가, 여러 링크 섹션이 있는 푸터에는 기본값 horizontal(wrap 가능한 한 줄)이 맞습니다. 반대로 고르면 섹션 하나가 한쪽으로 치우치거나, 여러 섹션이 불필요하게 세로로 길게 늘어집니다.


❌ Don't#

semanticLabel에 빈 문자열 전달 금지

// ❌ 빈 문자열은 플랫폼마다 다르게 처리됨
Footer(semanticLabel: '', sections: const [])

// ✅ 라벨을 직접 정하지 않을 거라면 생략 — 로케일 폴백이 채움
Footer(sections: const [])

semanticLabel: ''을 넘기면 Flutter는 이름 없는 랜드마크를 역할째로 떨어뜨려 푸터가 랜드마크 목록에서 사라지고, Web은 role="contentinfo"를 유지한 채 이름만 빈 상태가 되어 두 플랫폼의 접근성 트리가 갈립니다.

접근성 (Accessibility)#

역할#

양 플랫폼 모두 contentinfo 랜드마크를 emit 하고, 이름도 아무것도 넘기지 않은 상태에서 붙습니다.

플랫폼역할이름
Flutter SemanticsRole.contentInfo ( CoUISemantics(role: .contentInfo, container: true) ) semanticLabel , 없으면 로케일 폴백( footerLabel — Footer / 바닥글 )
Web <footer role="contentinfo"> (중첩이면 tag: 'div') aria-label = semanticLabel , 없으면 같은 로케일 폴백( footerLabel )

폴백은 바닥이지 덮개가 아닙니다 — semanticLabel 을 넘기면 그 이름이 이기고, Web 에서는 호출자가 attributes 로 직접 넣은 aria-label 도 폴백을 덮습니다.

키보드#

Footer 자체가 처리하는 키는 없습니다. 키보드 인터랙션은 호출자가 넣은 자식들이 각자 가져옵니다 — aside / CoreFooterNavData.children / links 의 링크·버튼, 그리고 businessInfo 를 넘겼을 때 합성되는 Collapsible 의 트리거(Enter / Space 로 토글)입니다.

포커스 관리#

Footer 는 포커스를 받지도 관리하지도 않습니다 — FocusNode / tabindex / 포커스 링이 없습니다. 포커스 가능한 요소는 호출자가 넣은 자식들과, 디스클로저를 쓸 때 Collapsible 이 스스로 만드는 트리거뿐입니다.

스크린 리더#

랜드마크 목록에는 양 플랫폼 모두 이름 붙은 contentinfo 로 나타납니다. 섹션 제목(CoreFooterNavData.title)은 일반 텍스트로 읽힙니다 — Flutter 는 Text, Web 은 <div> 라 heading 이 아닙니다.

사업자 정보 디스클로저의 펼침 상태는 합성된 Collapsible 이 알립니다 — Web 은 트리거에 aria-expanded, Flutter 는 그 컴포넌트의 시맨틱을 그대로 씁니다. 푸터는 여기에 아무것도 덧붙이지 않습니다.

알려진 제약#

  • 섹션 제목에 heading 레벨이 없습니다(h2~h6 / SemanticsRole.header 미emit). 스크린 리더 사용자가 heading 단위로 푸터 섹션을 건너뛸 수 없습니다.
  • 링크 그룹에 내비게이션·리스트 구조가 없습니다. CoreFooterNavData 라는 이름과 달리 role="navigation" / <nav> 도, list / listitem 구조도 emit 하지 않아 "링크 5개" 같은 개수 안내가 나오지 않습니다. 법적 고지 블록의 links 줄도 같습니다.
  • 행 안의 hairline 눈금은 장식입니다. 회사명↔디스클로저, 링크↔링크 사이의 세로 눈금은 시각 구분자일 뿐이라 스크린 리더가 항목 경계를 읽어주지 않습니다 — 항목 사이의 구분은 눈으로만 전달됩니다.
  • businessInfoLabel 이 라벨을 다 말해야 합니다. 트리거는 이 슬롯의 텍스트와 Collapsible 이 그린 셰브론뿐이라, 라벨이 "자세히" 처럼 무엇을 여는지 말하지 않으면 스크린 리더에서 그 트리거는 맥락 없이 읽힙니다.
  • semanticLabel: ''(빈 문자열)은 두 플랫폼이 다르게 처리합니다. Flutter 는 이름 없는 랜드마크가 아무 말도 못 하므로 역할 자체를 떨어뜨려(→ 전역 접근성 축 의 역할 전제조건) 푸터가 랜드마크 목록에서 사라지고, Web 은 role="contentinfo" 를 유지한 채 이름만 빈 랜드마크가 됩니다. 이름을 직접 정하지 않을 거라면 빈 문자열 대신 생략하세요 — 그때 폴백이 채웁니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
레이아웃 구현 Wrap / Column flex (flex-wrap / flex-col)
루트 요소 OutlinedContainer + CoUISemantics(role: .contentInfo) 진짜 <footer> (tag 로 교체 가능 — 중첩 시 tag: 'div')
회사명 / 저작권 주변 텍스트 스타일 DefaultTextStyle.merge 슬롯을 감싼 <div> 의 typography class
사업자 정보 디스클로저 Collapsible (Flexible 안에서 남은 폭을 차지) Collapsible (flex-1 min-w-0)
링크 줄 줄바꿈 Wrap (linkRowSpacing 이 가로·세로 양쪽) flex + gap
  • NavigationBar: 상단 내비게이션
  • Dock: 모바일 하단 탭 내비게이션
  • Collapsible: 사업자 정보 디스클로저가 합성하는 컴포넌트 — 셰브론·펼침 애니메이션·상태를 소유
  • Divider: 상단 구분선과 법적 고지 행 안의 hairline 눈금
  • Link: links 줄에 넣는 항목
  • Gap: 섹션 제목과 본문 사이
  • Text: 제목과 본문이 상속하는 글자