화면 상단에서 현재 화면의 제목과 탐색 액션을 보여주는 내비게이션 바 컴포넌트입니다.
@seed-design/lynx-react@0.2.0, @seed-design/lynx-css@0.2.0
import "./styles" ;
import IconBellLine from "@karrotmarket/lynx-monochrome-icon/IconBellLine" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import {
AppBar,
AppBarBackButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-app-bar-root` }>
< view className = "app-bar-preview" >
< AppBar theme = "cupertino" >
< AppBarLeft >
< AppBarBackButton />
</ AppBarLeft >
< AppBarMain title = "동네생활" />
< AppBarRight >
< AppBarIconButton accessibility-label = "알림" icon = {< IconBellLine />} />
</ AppBarRight >
</ AppBar >
< view className = "app-bar-preview__content" >
< text className = "app-bar-preview__status" >화면 콘텐츠</ text >
</ view >
</ view >
</ view >
);
}
npx @seed-design/cli add ui:app-bar pnpm dlx @seed-design/cli add ui:app-bar yarn dlx @seed-design/cli add ui:app-bar bun x @seed-design/cli add ui:app-bar
의존성 설치
npm install @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react yarn add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react pnpm add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react bun add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react 아래 코드를 복사 후 붙여넣고 사용하세요 /**
* @file ui:app-bar
* @requires @seed-design/lynx-react@>=0.1.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.1.0 <1.0.0
**/
import IconChevronLeftLine from "@karrotmarket/lynx-monochrome-icon/IconChevronLeftLine" ;
import IconXmarkLine from "@karrotmarket/lynx-monochrome-icon/IconXmarkLine" ;
import { forwardRef, type ReactNode } from "@lynx-js/react" ;
import { AppBar as SeedAppBar } from "@seed-design/lynx-react" ;
export interface AppBarProps extends SeedAppBar . RootProps {}
/**
* @see https://seed-design.io/lynx/components/app-bar
*/
export const AppBar = SeedAppBar.Root;
export interface AppBarLeftProps extends SeedAppBar . LeftProps {}
export const AppBarLeft = SeedAppBar.Left;
export interface AppBarRightProps extends SeedAppBar . RightProps {}
export const AppBarRight = SeedAppBar.Right;
export interface AppBarSlotProps extends SeedAppBar . SlotProps {}
export const AppBarSlot = SeedAppBar.Slot;
export interface AppBarMainProps extends SeedAppBar . MainProps {
/**
* The title of the app bar.
* If custom children are provided, this prop will be ignored.
*/
title ?: ReactNode ;
/**
* The subtitle of the app bar.
* If custom children are provided, this prop will be ignored.
*/
subtitle ?: ReactNode ;
}
function shouldRenderCustomMain ( children : ReactNode ) {
return (
children != null &&
typeof children !== "string" &&
typeof children !== "number" &&
typeof children !== "boolean"
);
}
export const AppBarMain = forwardRef < unknown , AppBarMainProps >(
({ title , subtitle , children , ... otherProps }, ref ) => {
if ( shouldRenderCustomMain (children)) {
return (
< SeedAppBar.Main ref = {ref} { ... otherProps}>
{children}
</ SeedAppBar.Main >
);
}
return (
< SeedAppBar.Main ref = {ref} layout = {subtitle ? "withSubtitle" : "titleOnly" } { ... otherProps}>
< SeedAppBar.Title >{children ?? title}</ SeedAppBar.Title >
{subtitle ? < SeedAppBar.Subtitle >{subtitle}</ SeedAppBar.Subtitle > : null }
</ SeedAppBar.Main >
);
},
);
AppBarMain.displayName = "AppBarMain" ;
export interface AppBarIconButtonProps extends SeedAppBar . IconButtonProps {}
export const AppBarIconButton = SeedAppBar.IconButton;
export interface AppBarBackButtonProps extends Omit < AppBarIconButtonProps , "icon" > {
icon ?: AppBarIconButtonProps [ "icon" ];
}
export const AppBarBackButton = forwardRef < unknown , AppBarBackButtonProps >(
(
{
icon = < IconChevronLeftLine />,
"accessibility-label" : accessibilityLabel = "뒤로" ,
... otherProps
},
ref,
) => {
return (
< AppBarIconButton
ref = {ref}
accessibility-label = {accessibilityLabel}
icon = {icon}
{ ... otherProps}
/>
);
},
);
AppBarBackButton.displayName = "AppBarBackButton" ;
export interface AppBarCloseButtonProps extends Omit < AppBarIconButtonProps , "icon" > {
icon ?: AppBarIconButtonProps [ "icon" ];
}
export const AppBarCloseButton = forwardRef < unknown , AppBarCloseButtonProps >(
(
{ icon = < IconXmarkLine />, "accessibility-label" : accessibilityLabel = "닫기" , ... otherProps },
ref,
) => {
return (
< AppBarIconButton
ref = {ref}
accessibility-label = {accessibilityLabel}
icon = {icon}
{ ... otherProps}
/>
);
},
);
AppBarCloseButton.displayName = "AppBarCloseButton" ;
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
bindlayoutchange?EventHandler < LayoutChangeDetailEvent < Target >> | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
layout?"titleOnly" | "withSubtitle" | undefined
theme?"cupertino" | "android" | undefined
transitionStyle?"slideFromRightIOS" | "fadeFromBottomAndroid" | "fadeIn" | undefined
tone?"layer" | "transparent" | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
bindlayoutchange?EventHandler < LayoutChangeDetailEvent < Target >> | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
icon?ReactElement < LynxIconElementProps, string | JSXElementConstructor < any >> | undefined
accessibility-label?string | undefined
accessibility-element?boolean | undefined
accessibility-traits?"text" | "image" | "button" | "link" | "header" | "search" | "selected" | "playable" | "keyboard" | "summary" | "disabled" | "updating" | "adjustable" | "tabbar" | "none" | undefined
className?string | undefined
children?ReactNode
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
main-thread:bindtap?EventHandler < BaseTouchEvent < Element >> | undefined
icon?ReactElement < LynxIconElementProps, string | JSXElementConstructor < any >> | undefined
className?string | undefined
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
main-thread:bindtap?EventHandler < BaseTouchEvent < Element >> | undefined
children?ReactNode
accessibility-label?string | undefined
accessibility-element?boolean | undefined
accessibility-traits?"text" | "image" | "button" | "link" | "header" | "search" | "selected" | "playable" | "keyboard" | "summary" | "disabled" | "updating" | "adjustable" | "tabbar" | "none" | undefined
icon?ReactElement < LynxIconElementProps, string | JSXElementConstructor < any >> | undefined
children?ReactNode
className?string | undefined
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
main-thread:bindtap?EventHandler < BaseTouchEvent < Element >> | undefined
accessibility-label?string | undefined
accessibility-element?boolean | undefined
accessibility-traits?"disabled" | "text" | "image" | "button" | "link" | "header" | "search" | "selected" | "playable" | "keyboard" | "summary" | "updating" | "adjustable" | "tabbar" | "none" | undefined
설치한 snippet은 Root와 주요 슬롯을 AppBar, AppBarLeft, AppBarMain, AppBarRight로 export 합니다. AppBarBackButton, AppBarCloseButton은 기본 아이콘만 제공하며, 실제 이동 로직은 bindtap으로 직접 전달합니다.
import {
AppBar,
AppBarBackButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export function Header ({ goBack } : { goBack : () => void }) {
return (
< AppBar >
< AppBarLeft >
< AppBarBackButton bindtap = {goBack} />
</ AppBarLeft >
< AppBarMain title = "동네생활" />
< AppBarRight />
</ AppBar >
);
}
AppBarMain에 title, subtitle을 전달하면 내부에서 AppBar.Title, AppBar.Subtitle을 조립합니다. subtitle이 있으면 layout="withSubtitle"가 자동으로 적용됩니다.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { AppBar, AppBarMain } from "@/components/ui/app-bar" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-app-bar-root` }>
< view className = "app-bar-preview" >
< AppBar theme = "cupertino" >
< AppBarMain title = "관심 목록" subtitle = "3개의 새 소식" />
</ AppBar >
< view className = "app-bar-preview__content" >
< text className = "app-bar-preview__status" >제목과 부제목을 함께 표시한 AppBar</ text >
</ view >
</ view >
</ view >
);
}
직접 슬롯을 조립하고 싶다면 AppBarMain의 children으로 compound 컴포넌트를 전달합니다.
import { AppBar, AppBarMain } from "@/components/ui/app-bar" ;
import { AppBar as SeedAppBar } from "@seed-design/lynx-react" ;
export function Header () {
return (
< AppBar >
< AppBarMain layout = "withSubtitle" >
< SeedAppBar.Title >제목</ SeedAppBar.Title >
< SeedAppBar.Subtitle >부제목</ SeedAppBar.Subtitle >
</ AppBarMain >
</ AppBar >
);
}
왼쪽과 오른쪽에는 icon button 또는 custom slot을 배치할 수 있습니다. Cupertino theme에서는 좌우 슬롯 폭을 읽어 가운데 title이 치우치지 않도록 padding을 보정합니다.
AppBarLeft와 AppBarRight 모두 첫 번째 자식이 아이콘 버튼이면 왼쪽, 마지막 자식이 아이콘 버튼이면 오른쪽 여백을 보정합니다. 버튼 하나만 있으면 양쪽을 모두 보정합니다. 44px 터치 영역과 24px 아이콘 사이의 여백만큼 음수 margin을 적용해, 터치 영역을 유지하면서 화면 가장자리와 제목 쪽 간격을 React의 슬롯 보정 규칙에 맞춥니다.
슬롯이 가장자리 위치를 Context로 전달하므로 AppBarBackButton / AppBarCloseButton과 직접 만든 AppBarIconButton에 자동으로 적용됩니다. Fragment와 조건부 자식을 지원하며, AppBarSlot이나 native 레이아웃 박스 내부에는 자동 보정을 전달하지 않습니다. edge="leading" | "trailing" | "both"를 명시하면 자동 보정 방향을 덮어쓸 수 있습니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarLeft,
AppBarMain,
AppBarRight,
AppBarSlot,
} from "@/components/ui/app-bar" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ lastAction , setLastAction ] = useState ( "없음" );
function handleBack () {
"background only" ;
setLastAction ( "뒤로" );
}
function handleClose () {
"background only" ;
setLastAction ( "닫기" );
}
return (
< view className = { `${ seedClassName } docs-lynx-app-bar-root` }>
< view className = "app-bar-preview" >
< AppBar theme = "cupertino" >
< AppBarLeft >
< AppBarBackButton bindtap = {handleBack} />
</ AppBarLeft >
< AppBarMain title = "작성하기" />
< AppBarRight >
< AppBarSlot >
< text >완료</ text >
</ AppBarSlot >
< AppBarCloseButton bindtap = {handleClose} />
</ AppBarRight >
</ AppBar >
< view className = "app-bar-preview__content" >
< text className = "app-bar-preview__status" >마지막 액션: {lastAction}</ text >
</ view >
</ view >
</ view >
);
}
AppBarBackButton / AppBarCloseButton 외에 다른 아이콘을 쓰고 싶다면 AppBarIconButton의 icon prop에 Lynx 아이콘을 전달해 직접 구성합니다. 가장자리에 놓이면 위와 동일하게 자동으로 정렬됩니다.
import IconBellLine from "@karrotmarket/lynx-monochrome-icon/IconBellLine" ;
import {
AppBar,
AppBarBackButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export function Header ({ goBack } : { goBack : () => void }) {
return (
< AppBar >
< AppBarLeft >
< AppBarBackButton bindtap = {goBack} />
</ AppBarLeft >
< AppBarMain title = "동네생활" />
< AppBarRight >
< AppBarIconButton accessibility-label = "알림" icon = {< IconBellLine />} />
</ AppBarRight >
</ AppBar >
);
}
슬롯에 여러 버튼을 넣으면 첫 번째와 마지막 버튼에만 가장자리 여백 보정이 적용됩니다. 가운데 버튼의 터치 영역은 그대로 유지됩니다. 조건부 렌더링으로 버튼이 하나만 남으면 양쪽 여백 보정으로 자동 변경됩니다.
아래 예제에서 검색·닫기를 숨기거나 다시 표시해도 제목은 가운데에 유지됩니다. 각 아이콘 버튼을 눌러 마지막 액션도 확인할 수 있습니다.
import "./styles" ;
import IconBellLine from "@karrotmarket/lynx-monochrome-icon/IconBellLine" ;
import IconMagnifyingglassLine from "@karrotmarket/lynx-monochrome-icon/IconMagnifyingglassLine" ;
import { useState } from "@lynx-js/react" ;
import { ActionButton, useSeedClassName } from "@seed-design/lynx-react" ;
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ showExtraActions , setShowExtraActions ] = useState ( true );
const [ lastAction , setLastAction ] = useState ( "없음" );
function toggleExtraActions () {
"background only" ;
setShowExtraActions (( value ) => ! value);
}
function recordAction ( action : string ) {
"background only" ;
setLastAction (action);
}
return (
< view className = { `${ seedClassName } docs-lynx-app-bar-root` }>
< view className = "app-bar-preview" >
< AppBar theme = "cupertino" >
< AppBarLeft >
< AppBarBackButton bindtap = {() => recordAction ( "뒤로" )} />
</ AppBarLeft >
< AppBarMain title = "동네생활" />
< AppBarRight >
< AppBarIconButton
accessibility-label = "알림"
icon = {< IconBellLine />}
bindtap = {() => recordAction ( "알림" )}
/>
{showExtraActions && (
<>
< AppBarIconButton
accessibility-label = "검색"
icon = {< IconMagnifyingglassLine />}
bindtap = {() => recordAction ( "검색" )}
/>
< AppBarCloseButton bindtap = {() => recordAction ( "닫기" )} />
</>
)}
</ AppBarRight >
</ AppBar >
< view className = "app-bar-preview__content app-bar-preview__controls" >
< ActionButton size = "small" variant = "neutralWeak" bindtap = {toggleExtraActions}>
{showExtraActions ? "검색·닫기 숨기기" : "검색·닫기 표시" }
</ ActionButton >
< text className = "app-bar-preview__status" >마지막 액션: {lastAction}</ text >
</ view >
</ view >
</ view >
);
}
직접 만든 레이아웃 박스나 AppBarSlot 안의 버튼에는 가장자리 보정을 자동 적용하지 않습니다. 내부 배치를 직접 관리할 때만 edge로 방향을 지정하세요. 일반적인 AppBarLeft / AppBarRight의 버튼에는 지정할 필요가 없습니다.
< AppBarRight >
< AppBarSlot >
< AppBarCloseButton edge = "trailing" bindtap = {close} />
</ AppBarSlot >
</ AppBarRight >
leading은 왼쪽, trailing은 오른쪽, both는 양쪽 여백을 보정합니다. 명시한 값은 자동 보정보다 우선하며, 버튼의 터치 영역 크기는 바뀌지 않습니다.
기본값인 tone="layer"는 레이어 배경을 표시합니다. tone="transparent"는 배경을 투명하게 하고 제목과 아이콘에 밝은 색상을 적용하므로 사진이나 어두운 배경 위에서 사용하세요. 배경과 스크롤에 따른 tone 전환은 호출부에서 관리합니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ lastAction , setLastAction ] = useState ( "배경 위에 겹쳐지는 AppBar" );
function handleBack () {
"background only" ;
setLastAction ( "뒤로 버튼을 눌렀습니다" );
}
function handleClose () {
"background only" ;
setLastAction ( "닫기 버튼을 눌렀습니다" );
}
return (
< view className = { `${ seedClassName } docs-lynx-app-bar-root` }>
< view className = "app-bar-preview app-bar-preview--transparent" >
< AppBar theme = "cupertino" tone = "transparent" >
< AppBarLeft >
< AppBarBackButton bindtap = {handleBack} />
</ AppBarLeft >
< AppBarMain title = "사진 보기" />
< AppBarRight >
< AppBarCloseButton bindtap = {handleClose} />
</ AppBarRight >
</ AppBar >
< view className = "app-bar-preview__content" >
< text className = "app-bar-preview__overlay-text" >{lastAction}</ text >
</ view >
</ view >
</ view >
);
}
Lynx에서는 HTML ARIA 속성 대신 Lynx native 접근성 속성을 사용합니다. AppBarIconButton은 기본적으로 accessibility-element={true}와 accessibility-traits="button"을 적용하며, 아이콘만으로 의미를 알 수 없는 버튼에는 accessibility-label을 전달해야 합니다.
import IconBellLine from "@karrotmarket/lynx-monochrome-icon/IconBellLine" ;
< AppBarIconButton accessibility-label = "알림" icon = {< IconBellLine />} />
AppBarBackButton, AppBarCloseButton은 각각 "뒤로", "닫기" 라벨을 기본값으로 제공합니다.
theme은 "cupertino" 또는 "android"를 사용할 수 있습니다. 명시하지 않으면 AppBar 내부에서 SystemInfo.platform을 읽어 Android에서는 "android", 그 외 플랫폼이나 값이 없을 때는 "cupertino"를 사용합니다.
< AppBar >
< AppBarMain title = "자동 theme" />
</ AppBar >
수동으로 지정한 theme은 platform 기본값보다 우선합니다.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-app-bar-root` }>
< view className = "app-bar-preview__platforms" >
< view className = "app-bar-preview__platform" >
< text className = "app-bar-preview__label" >Cupertino</ text >
< view className = "app-bar-preview app-bar-preview--platform" >
< AppBar theme = "cupertino" >
< AppBarLeft >
< AppBarBackButton />
</ AppBarLeft >
< AppBarMain title = "화면 제목" subtitle = "보조 제목" />
< AppBarRight >
< AppBarCloseButton />
</ AppBarRight >
</ AppBar >
</ view >
</ view >
< view className = "app-bar-preview__platform" >
< text className = "app-bar-preview__label" >Android</ text >
< view className = "app-bar-preview app-bar-preview--platform" >
< AppBar theme = "android" >
< AppBarLeft >
< AppBarBackButton />
</ AppBarLeft >
< AppBarMain title = "화면 제목" subtitle = "보조 제목" />
< AppBarRight >
< AppBarCloseButton />
</ AppBarRight >
</ AppBar >
</ view >
</ view >
</ view >
</ view >
);
}
Lynx AppBar는 React AppBar와 같은 역할과 variant 이름을 제공하지만, 플랫폼별 Recipe와 화면 전환 책임은 다릅니다.
이벤트 핸들링 : onClick 대신 Lynx 이벤트인 bindtap을 전달합니다.
렌더링 요소 : HTML 요소 대신 네이티브 <view> / <text> 요소를 렌더링합니다.
접근성 라벨링 : aria-label 대신 Lynx native accessibility-label을 사용합니다.
platform theme : 웹처럼 data-* selector에 의존하지 않고, recipe variant className으로 "cupertino" / "android" 디자인을 적용합니다.
navigation 책임 : AppBarBackButton과 AppBarCloseButton은 이동을 자동 실행하지 않습니다. Stackflow pop() 같은 동작은 호출부에서 연결합니다.
가장자리 아이콘 정렬 : 웹은 iconButton:first-child / :last-child selector로 bleed를 보정하지만, Lynx는 해당 가상 클래스를 지원하지 않아 AppBarLeft / AppBarRight가 가장자리 위치를 Context로 전달하고 아이콘 버튼의 Recipe class로 보정합니다. 다중 버튼·custom slot에서도 가장자리 아이콘 버튼에만 정확히 적용됩니다.
현재 Lynx AppBar는 순수 UI 컴포넌트로 제공되며 다음 기능은 포함하지 않습니다.
기능 웹 대응 대체 방법 Stackflow activity 연동 useActivity, useActions 기반 back/close 처리bindtap에서 앱의 navigation 함수를 직접 호출swipe-back 전환 애니메이션 Stackflow screen transition 화면 전환을 소유한 navigation 계층에서 처리 data-* selector 기반 상태 스타일웹 recipe selector theme, tone variant className 사용HTML button 속성 type, DOM click eventLynx <view> 기반 AppBarIconButton과 bindtap 사용 HTML ARIA 라벨 aria-labelLynx native accessibility-label 사용