HextaUI

Marker

日付の区切りやシステムイベントのように、コンテンツの間に置く控えめなメモです。日付と時刻はスティッキーで、「今日」や「昨日」と表示されます。

Mira added Jun and Sol
Kicking off the marker component. Dates, events, the works.
Can the dates stick while you scroll?
Jun renamed the thread to “Marker”
They do now. Scroll up and the date turns into a pill.
Sol pinned a message
Docs are written.
Screenshots look right in dark mode too.
Sol merged #482 into main
New messages
Shipping it 🎉
"use client"

import * as React from "react"
import {
  IconGitMerge,
  IconPencil,
  IconPin,
  IconUserPlus,
} from "@tabler/icons-react"

import { Bubble, BubbleContent, BubbleGroup } from "@/components/ui/bubble"
import {
  Marker,
  MarkerContent,
  MarkerIcon,
  MarkerTime,
} from "@/components/ui/marker"

const day = 24 * 60 * 60 * 1000

export function MarkerDemo() {
  const threadRef = React.useRef<HTMLDivElement>(null)

  React.useLayoutEffect(() => {
    const thread = threadRef.current
    if (thread) {
      thread.scrollTop = thread.scrollHeight
    }
  }, [])

  return (
    <div
      ref={threadRef}
      tabIndex={0}
      role="region"
      aria-label="Conversation"
      className="h-96 w-full max-w-md overflow-y-auto overscroll-none rounded-xl border px-4 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
    >
      <section className="flex flex-col gap-3 pt-4 pb-3">
        <Marker variant="separator" sticky>
          <MarkerContent>
            <MarkerTime date={new Date(Date.now() - 2 * day)} />
          </MarkerContent>
        </Marker>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconUserPlus />
          </MarkerIcon>
          <MarkerContent>Mira added Jun and Sol</MarkerContent>
        </Marker>
        <Bubble variant="secondary">
          <BubbleContent>
            Kicking off the marker component. Dates, events, the works.
          </BubbleContent>
        </Bubble>
        <Bubble align="end">
          <BubbleContent>Can the dates stick while you scroll?</BubbleContent>
        </Bubble>
      </section>
      <section className="flex flex-col gap-3 py-3">
        <Marker variant="separator" sticky>
          <MarkerContent>
            <MarkerTime date={new Date(Date.now() - day)} />
          </MarkerContent>
        </Marker>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconPencil />
          </MarkerIcon>
          <MarkerContent>Jun renamed the thread to “Marker”</MarkerContent>
        </Marker>
        <Bubble variant="secondary">
          <BubbleContent>
            They do now. Scroll up and the date turns into a pill.
          </BubbleContent>
        </Bubble>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconPin />
          </MarkerIcon>
          <MarkerContent>Sol pinned a message</MarkerContent>
        </Marker>
      </section>
      <section className="flex flex-col gap-3 pt-3 pb-4">
        <Marker variant="separator" sticky>
          <MarkerContent>
            <MarkerTime date={new Date()} />
          </MarkerContent>
        </Marker>
        <BubbleGroup>
          <Bubble align="end">
            <BubbleContent>Docs are written.</BubbleContent>
          </Bubble>
          <Bubble align="end">
            <BubbleContent>
              Screenshots look right in dark mode too.
            </BubbleContent>
          </Bubble>
        </BubbleGroup>
        <Marker className="justify-center">
          <MarkerIcon>
            <IconGitMerge className="text-success" />
          </MarkerIcon>
          <MarkerContent>
            Sol merged <a href="#">#482</a> into main
          </MarkerContent>
        </Marker>
        <Marker variant="separator">
          <MarkerContent>New messages</MarkerContent>
        </Marker>
        <Bubble variant="secondary">
          <BubbleContent>Shipping it 🎉</BubbleContent>
        </Bubble>
      </section>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/marker.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

import {
  Marker,
  MarkerContent,
  MarkerIcon,
  MarkerTime,
} from "@/components/ui/marker"
<Marker variant="separator" sticky>
  <MarkerContent>
    <MarkerTime date={message.sentAt} />
  </MarkerContent>
</Marker>
Marker
├── MarkerIcon
└── MarkerContent
    └── MarkerTime

バリアント

default は控えめな注記、separator は2本の細い線の間に中央配置、border はセクション見出しのように下線を引きます。

import { IconInfoCircle } from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerVariants() {
  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <Marker>
        <MarkerIcon>
          <IconInfoCircle />
        </MarkerIcon>
        <MarkerContent>Default: an icon and a short note.</MarkerContent>
      </Marker>
      <Marker variant="separator">
        <MarkerContent>Separator</MarkerContent>
      </Marker>
      <Marker variant="border">
        <MarkerContent>Border: a heading for the section below</MarkerContent>
      </Marker>
    </div>
  )
}

固定日付

sticky を指定すると、マーカーはスクロール領域の上端に留まります。固定されると線がフェードして、ラベルが浮かぶピルに変わり、メッセージの上でも読みやすくなります。次の日付が前の日付を押し出すように、日ごとに別々のセクションに入れてください。

import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Marker, MarkerContent, MarkerTime } from "@/components/ui/marker"

const day = 24 * 60 * 60 * 1000

const days = [
  {
    date: new Date(Date.now() - 3 * day),
    messages: [
      "Kicking off the marker component today.",
      "Shadcn's version is a good base. Let's add sticky dates.",
      "Agreed, chats need them.",
    ],
  },
  {
    date: new Date(Date.now() - day),
    messages: [
      "Sticky dates work. They turn into a pill when they stick.",
      "Nice. Does it respect reduced motion?",
      "Yes, the pill just appears.",
    ],
  },
  {
    date: new Date(),
    messages: ["Docs are up.", "Shipping it.", "🎉"],
  },
]

export function MarkerSticky() {
  return (
    <div
      tabIndex={0}
      role="region"
      aria-label="Activity by day"
      className="h-80 w-full max-w-md overflow-y-auto overscroll-none rounded-xl border px-4 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden"
    >
      {days.map((group) => (
        <section
          key={group.date.toDateString()}
          className="flex flex-col gap-3 py-3"
        >
          <Marker variant="separator" sticky>
            <MarkerContent>
              <MarkerTime date={group.date} />
            </MarkerContent>
          </Marker>
          {group.messages.map((message, index) => (
            <Bubble
              key={message}
              align={index % 2 === 0 ? "start" : "end"}
              variant={index % 2 === 0 ? "secondary" : "default"}
            >
              <BubbleContent>{message}</BubbleContent>
            </Bubble>
          ))}
        </section>
      ))}
    </div>
  )
}

相対日付

<MarkerTime /> は、今日、昨日、直近1週間以内の曜日、それ以前は短い日付と読める <time> を描画します。深夜0時に自動で更新されます。独自の文言にするには format を渡します。

"use client"

import { Marker, MarkerContent, MarkerTime } from "@/components/ui/marker"

const day = 24 * 60 * 60 * 1000
const dates = [0, 1, 3, 40, 400].map((ago) => new Date(Date.now() - ago * day))

export function MarkerTimeDemo() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      {dates.map((date) => (
        <Marker key={date.getTime()} variant="separator">
          <MarkerContent>
            <MarkerTime date={date} />
          </MarkerContent>
        </Marker>
      ))}
      <Marker variant="separator">
        <MarkerContent>
          <MarkerTime
            date={dates[0]}
            format={(date) =>
              date.toLocaleTimeString("en-US", {
                hour: "numeric",
                minute: "2-digit",
              })
            }
          />
        </MarkerContent>
      </Marker>
    </div>
  )
}

アクティビティ

アクティビティログ向けに、マーカーをリスト項目として描画します。内部のリンクはホバーされるまで下線が付きます。

import {
  IconGitMerge,
  IconLock,
  IconPencil,
  IconTag,
} from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerEvents() {
  return (
    <ol className="flex w-full max-w-md flex-col gap-3">
      <Marker render={<li />}>
        <MarkerIcon>
          <IconTag />
        </MarkerIcon>
        <MarkerContent>
          Jun added the <a href="#">design</a> label
        </MarkerContent>
      </Marker>
      <Marker render={<li />}>
        <MarkerIcon>
          <IconPencil />
        </MarkerIcon>
        <MarkerContent>Mira renamed the pull request</MarkerContent>
      </Marker>
      <Marker render={<li />}>
        <MarkerIcon>
          <IconGitMerge className="text-success" />
        </MarkerIcon>
        <MarkerContent>Sol merged into main</MarkerContent>
      </Marker>
      <Marker render={<li />}>
        <MarkerIcon>
          <IconLock />
        </MarkerIcon>
        <MarkerContent>Conversation locked</MarkerContent>
      </Marker>
    </ol>
  )
}

長いコンテンツ

注記は折り返され、区切りのない文字列はレイアウトを広げずに改行されます。区切り線は両側に最小の長さを保ちます。

import { IconAlertTriangle } from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerLongContent() {
  return (
    <div className="flex w-72 max-w-full flex-col gap-6">
      <Marker>
        <MarkerIcon>
          <IconAlertTriangle className="text-warning" />
        </MarkerIcon>
        <MarkerContent>
          Messages older than 90 days were archived to
          [email protected]
        </MarkerContent>
      </Marker>
      <Marker variant="separator">
        <MarkerContent>
          A separator label that is long enough to wrap onto two lines
        </MarkerContent>
      </Marker>
    </div>
  )
}

右から左

アイコンとテキストは文字の読み方向に従います。

import { IconUserPlus } from "@tabler/icons-react"

import { Marker, MarkerContent, MarkerIcon } from "@/components/ui/marker"

export function MarkerRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-md flex-col gap-4">
      <Marker variant="separator">
        <MarkerContent>اليوم</MarkerContent>
      </Marker>
      <Marker>
        <MarkerIcon>
          <IconUserPlus />
        </MarkerIcon>
        <MarkerContent>أضافت ميرا جون إلى المحادثة</MarkerContent>
      </Marker>
    </div>
  )
}
  • マーカーはプレーンテキストなので、スクリーンリーダーはその場で読み上げます。アイコンは読み上げから隠されます。
  • <MarkerTime /> は dateTime に正確な時刻を保持しつつ、わかりやすいラベルを表示します。
  • 視差効果の軽減が有効な場合、固定されたピルはアニメーションなしで表示されます。
プロパティ型デフォルト
variant
"default" | "separator" | "border""default"
stickyスクロール領域の上端に固定され、固定されている間はピル型になります。
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="marker"CSS でマーカーを指定します。
data-variant現在のバリアント。
data-stickysticky が有効なときに付きます。
data-stuckマーカーが上端に固定されている間付きます。
--marker-sticky-top固定されている間の上端からの距離。デフォルトは 0.5rem です。
属性説明
data-slot="marker-content"テキスト。固定されている間はピルになります。
属性説明
data-slot="marker-icon"16pxのアイコンボックス。スクリーンリーダーからは隠されます。
プロパティ型デフォルト
date必須。new Date() が受け付ける任意の値。
Date | string | number–
localeサーバーとブラウザーで一致するよう、デフォルトでは固定です。
Intl.LocalesArgument"en-US"
format相対ラベルを独自のものに置き換えます。
(date: Date) => ReactNode–
属性説明
data-slot="marker-time"time 要素。ISO 形式の dateTime を持ちます。