Footer | CoUI
LogoCoUI

Footer

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

Footer#

페이지 하단에 최대 세 블록을 세로로 쌓는 푸터입니다 — 옵션 상단 구분선(showDivider), 브랜딩(aside) + 다중 내비게이션 섹션으로 이루어진 내비게이션 밴드, 그리고 법적 고지 블록(회사명 · 사업자 정보 디스클로저 · 링크 줄 · 저작권). 각 블록은 그 슬롯이 비면 통째로 빠지므로, 내비게이션만 넘긴 푸터는 법적 고지 블록이 없던 때와 똑같이 그려집니다. 밴드는 horizontal/vertical/center 세 가지 레이아웃을 지원하며 배경/텍스트 색상은 토큰 기반으로 일관됩니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 사이트 최하단에 회사명/링크/저작권 정보를 배치할 때
  • 여러 카테고리(Services, Company, Legal)의 보조 링크를 모아놓을 때

대신 다른 컴포넌트를 사용하세요:

  • Dock: 모바일 하단 탭 내비게이션
  • NavigationBar: 데스크톱 상단 전역 내비게이션

기본 사용법 (Basic Usage)#

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;
  }
}

Props / Parameters#

속성타입기본값설명
sections List<CoreFooterNavData<W>> 필수 렌더링할 nav 섹션 목록
asideW?null브랜딩/로고 슬롯 (sections 앞에 배치)
layout CoreFooterLayout? horizontal horizontal/vertical/center
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? 'footer' 스크린리더 landmark label
footerStyle CoreFooterStyle? null 모든 chrome 이 지나는 단일 슬롯 (아래 표 참고)

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.

CoreFooterNavData<W>#

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

Resolve chain#

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

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

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

레이아웃 (Layouts)#

동작
horizontal (기본)sections + aside가 한 줄에 wrap. 공간 부족 시 자동 줄바꿈
verticalsections + aside가 위→아래로 stack, leading 정렬
center sections + aside가 위→아래로 stack, center 정렬 + text-center

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

스토어프론트가 공시해야 하는 정보 — 회사명, 사업자 등록 사항, 약관/개인정보/환불 링크, 저작권 — 를 담는 두 번째 블록입니다. 네 슬롯이 전부 비면 블록 자체가 그려지지 않으므로, 내비게이션만 쓰던 푸터는 아무것도 달라지지 않습니다.

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 입니다. 셰브론·펼침 애니메이션·controlled/uncontrolled 하이브리드 상태를 그 컴포넌트가 이미 소유하므로, 푸터는 라벨(businessInfoLabel)과 문단(businessInfo)만 넘깁니다. 여기에 별도 disclosure 를 손으로 짜지 마세요.
  • 트리거는 문단보다 오래 살 수 없습니다. businessInfo 가 null 이면 businessInfoLabel 을 넘겨도 트리거가 그려지지 않습니다 — 트리거는 그 문단을 여는 affordance 일 뿐이라, 열 것이 없으면 존재할 이유도 없습니다.
  • 행 안의 눈금은 Divider 입니다. 회사명↔디스클로저 사이, 그리고 링크 사이의 1px 세로 눈금은 전부 세로 Divider 이고 색·두께는 separatorStyle, 길이는 separatorLength 가 정합니다.

links 는 보통 Link 위젯을 담지만 타입은 W? 라 임의 위젯이 들어갑니다 — 눈금은 항목 사이에만 들어가므로 항목이 하나면 눈금은 그려지지 않습니다.

상단 구분선 (showDivider)#

내비게이션 밴드 위의 hairline 규칙은 기본이 꺼짐입니다. 디자이너 세트가 네 멤버 모두에서 이 선을 숨기고 "기본 꺼짐 옵션"으로 정의했기 때문에, 아무 말도 하지 않은 푸터는 선을 그리지 않습니다.

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

이 선은 full-bleed 입니다 — 밴드의 가로 패딩 바깥, 푸터의 양 끝까지 이어집니다. 색·두께는 dividerStyle 슬롯이 소유하며, 비워두면 Divider 자신의 기본값(outlineVariant / stroke1)이 그대로 쓰입니다.

사용 가이드라인 (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 , 없으면 로케일 폴백( footerLabelFooter / 바닥글 )
Web role="contentinfo" (루트 <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
클래스명FooterFooter
섹션 데이터 CoreFooterNavData<Widget> CoreFooterNavData<Component>
backgroundColor / textColorCoreColor?CoreColor?
paddingCoreEdgeInsets?CoreEdgeInsets?
레이아웃 구현 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 줄에 넣는 항목