Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions packages/demo/src/components/demo/format-date.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { PanelLabel, FormatDate, Card, CardContent } from "@eqtylab/equality";
import { useMemo } from "react";

const MINUTE = 60 * 1000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;

export function FormatDateRelativeDemo() {
// Compute offsets from "now" once on mount so the examples read naturally.
const samples = useMemo(() => {
const now = new Date();
const time = now.getTime();
return [
{ label: "Seconds ago", date: new Date(time - 10 * 1000) },
{ label: "Minutes ago", date: new Date(time - 5 * MINUTE) },
{ label: "Hours ago", date: new Date(time - 3 * HOUR) },
{ label: "Days ago", date: new Date(time - 2 * DAY) },
{ label: "Weeks ago", date: new Date(time - 2 * WEEK) },
{ label: "In the future", date: new Date(time + 4 * DAY) },
];
}, []);

return (
<div className="my-4">
<Card>
<CardContent>
{samples.map((sample) => (
<div key={sample.label} className="flex items-center gap-3">
<PanelLabel label={sample.label} className="w-28" />
<FormatDate date={sample.date} displayAs="relative" />
</div>
))}
</CardContent>
</Card>
</div>
);
}

export function FormatDateTimeZoneDemo() {
const date = useMemo(() => new Date("2026-06-09T18:42:03Z"), []);

return (
<div className="my-4">
<Card>
<CardContent className="divide-border divide-y divide-solid">
<div className="flex items-center gap-3 py-1">
<PanelLabel label="UTC (default)" className="w-28" />
<FormatDate date={date} displayAs="absolute" />
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="New York" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="America/New_York"
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="Tokyo" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="Asia/Tokyo"
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="Auckland" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="Pacific/Auckland"
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="Toronto" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="America/Toronto"
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="Los Angeles" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="America/Los_Angeles"
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="Buenos Aires" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="America/Buenos_Aires"
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="London" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
timeZone="Europe/London"
/>
</div>
</CardContent>
</Card>
</div>
);
}

export function FormatDateOptionsDemo() {
const date = useMemo(() => new Date("2026-06-09T18:42:03Z"), []);

// Each entry overrides the default Intl.DateTimeFormat options.
const samples: {
label: string;
options: Intl.DateTimeFormatOptions;
}[] = [
{
label: "Date only",
options: { year: "numeric", month: "long", day: "numeric" },
},
{
label: "Time only",
options: { hour: "2-digit", minute: "2-digit" },
},
{
label: "With weekday",
options: {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
},
},
{
label: "Short numeric",
options: { dateStyle: "short", timeStyle: "short" },
},
];

return (
<div className="my-4">
<Card>
<CardContent className="divide-border divide-y divide-solid">
{samples.map((sample) => (
<div key={sample.label} className="flex items-center gap-3 py-1">
<PanelLabel label={sample.label} className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
absoluteOptions={sample.options}
/>
</div>
))}
</CardContent>
</Card>
</div>
);
}

export function FormatDateIsoDemo() {
const date = useMemo(() => new Date("2026-06-09T18:42:03Z"), []);

// en-CA orders numeric dates as yyyy-mm-dd
// en-US would render dd/mm/yyyy
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "2-digit",
day: "2-digit",
};

return (
<div className="my-4">
<Card>
<CardContent className="divide-border divide-y divide-solid">
<div className="flex items-center gap-3 py-1">
<PanelLabel label="en-CA" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
locale="en-CA"
absoluteOptions={options}
/>
</div>
<div className="flex items-center gap-3 py-1">
<PanelLabel label="en-US" className="w-28" />
<FormatDate
date={date}
displayAs="absolute"
locale="en-US"
absoluteOptions={options}
/>
</div>
</CardContent>
</Card>
</div>
);
}
121 changes: 121 additions & 0 deletions packages/demo/src/content/components/format-date.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
layout: "@demo/layouts/mdx-layout.astro"
heading: "Format Date"
description: "Render a date as relative or absolute time using a semantic <time> element"
---

import { FormatDate } from "@eqtylab/equality";
import {
FormatDateIsoDemo,
FormatDateOptionsDemo,
FormatDateRelativeDemo,
FormatDateTimeZoneDemo,
} from "@demo/components/demo/format-date";

## Overview

The <code>FormatDate</code> component renders a date as either **relative** time (e.g. "2 weeks ago", "Just now") or **absolute** time (e.g. "Jun 9 2026, 18:42:03 UTC"). It always renders as a semantic HTML [`<time>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time) element with a `dateTime` attribute.

When showing relative time, a tooltip reveals the absolute time on hover or keyboard focus.

## Time zones

Absolute time is formatted in **UTC by default**. To render in a different zone, pass a `timeZone` (e.g. `"America/New_York"`). Relative time reads the same everywhere.

## Usage

Import the component:

```tsx
import { FormatDate } from "@eqtylab/equality";
```

Basic usage with the required `date` property, which accepts an ISO 8601 string, epoch milliseconds, or a `Date`. Each of these renders the same instant:

```tsx
<FormatDate date="2026-06-09T18:42:03Z" />
<FormatDate date={1781030523000} />
<FormatDate date={new Date("2026-06-09T18:42:03Z")} />
```

## Variants

### Absolute

The default. Renders the full date and time in the configured `timeZone` (UTC unless overridden).

<FormatDateTimeZoneDemo client:only="react" />

```tsx
<FormatDate date="2026-06-09T18:42:03Z" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="America/New_York" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="Asia/Tokyo" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="Pacific/Auckland" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="America/Toronto" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="America/Los_Angeles" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="America/Buenos_Aires" />
<FormatDate date="2026-06-09T18:42:03Z" timeZone="Europe/London" />
```

### Relative

Set `displayAs="relative"` to render distance from now. The value updates automatically on an interval, and hovering or focusing the date reveals the absolute time in a tooltip.

<FormatDateRelativeDemo client:only="react" />

```tsx
<FormatDate date={data.updatedAt} displayAs="relative" />
```

Disable the tooltip with `tooltip={false}`, or stop the auto-updating with `live={false}`.

### Custom formatting

Pass `absoluteOptions` to override the [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options) options used for absolute time. For example to show a date without a time, a time without a date, or a weekday.

<FormatDateOptionsDemo client:only="react" />

```tsx
<FormatDate
date="2026-06-09T18:42:03Z"
absoluteOptions={{ year: "numeric", month: "long", day: "numeric" }}
/>
<FormatDate
date="2026-06-09T18:42:03Z"
absoluteOptions={{ hour: "2-digit", minute: "2-digit" }}
/>
<FormatDate
date="2026-06-09T18:42:03Z"
absoluteOptions={{ weekday: "long", year: "numeric", month: "long", day: "numeric" }}
/>
<FormatDate
date="2026-06-09T18:42:03Z"
absoluteOptions={{ dateStyle: "short", timeStyle: "short" }}
/>
```

#### ISO 8601 short date

For a short numeric date like `2026-06-09`, use numeric options. Digit _order_ is decided by the `locale`, not the options, so the default `en-US` renders this as `06/09/2026`. Pair the preset with `locale="en-CA"` (which orders numerically as `yyyy-mm-dd`) to get true ISO 8601:

<FormatDateIsoDemo client:only="react" />

```tsx
<FormatDate
date="2026-06-09T18:42:03Z"
locale="en-CA"
absoluteOptions={{ year: "numeric", month: "2-digit", day: "2-digit" }}
/>
```

## Props

| Name | Description | Type | Default | Required |
| ----------------- | ----------------------------------------------------------------------- | ---------------------------- | ---------- | -------- |
| `date` | The date to display | `string`, `number`, `Date` | — | ✅ |
| `displayAs` | Render relative or absolute time | `relative`, `absolute` | `absolute` | ❌ |
| `timeZone` | Time zone used for absolute formatting | `string` | `UTC` | ❌ |
| `locale` | BCP 47 locale used for formatting | `string` | `en-US` | ❌ |
| `tooltip` | When relative, show a tooltip with the absolute time on hover/focus | `boolean` | `true` | ❌ |
| `live` | When relative, re-render on an interval so the value stays current | `boolean` | `true` | ❌ |
| `absoluteOptions` | Override the `Intl.DateTimeFormat` options used for absolute formatting | `Intl.DateTimeFormatOptions` | — | ❌ |
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@eqtylab/equality",
"description": "EQTYLab's component and token-based design system",
"homepage": "https://equality.eqtylab.io/",
"version": "2.0.1",
"version": "2.1.0",
"license": "Apache-2.0",
"keywords": [
"component library",
Expand Down
10 changes: 10 additions & 0 deletions packages/ui/src/components/format-date/format-date.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@reference '../../theme/theme.module.css';

.format-date {
@apply tabular-nums;
}

.format-date--interactive {
@apply focus-ring rounded-sm;
@apply cursor-default;
}
Loading
Loading