Byline
Author byline component for testimonials and profiles: an optional avatar beside a semibold name and a muted second line such as a job title or company. Testimonials render it as the quote's caption.
pnpm dlx shadcn add @01orbital/bylineimport type { ComponentProps } from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cn } from "cn"
type BylineSize = "md" | "sm"
type BylineState = { slot: "byline"; size: BylineSize }
type BylineAvatarState = { slot: "byline-avatar" }
/**
* A person's byline: an optional `BylineAvatar`, then `BylineName` with a
* `BylineDescription` under it, as a `div`. With an avatar it becomes two
* columns: the avatar spans both lines, 16px from the text. Under a quote,
* pass `render={<figcaption />}`; in a centered quote add `w-fit text-left` so it shrinks to fit
* and keeps its text left-aligned.
*
* At `size="md"` (the default) the name is 16px/24px semibold and the
* description 14px/20px muted, 2px apart; a 40px avatar matches the two
* lines. At `size="sm"` both lines are 12px/20px, the name in regular weight,
* the byline of a card footer.
*/
function Byline({
className,
size = "md",
render,
...props
}: useRender.ComponentProps<"div", BylineState> & {
/** The type size: `md` (the default) 16px over 14px, `sm` 12px over 12px for a card footer. */
size?: BylineSize
}) {
return useRender({
defaultTagName: "div",
render,
state: { slot: "byline", size },
props: mergeProps<"div">(
{
className: cn(
"group/byline grid items-center gap-x-4 gap-y-0.5 text-sm/5 has-data-[slot=byline-avatar]:grid-cols-[auto_1fr] data-[size=sm]:text-xs/5",
className
),
},
props
),
})
}
/**
* Holds the person's picture beside the name and description: a stock
* `Avatar`, an orb avatar or an image, 40px recommended. Use
* `render={<div />}` when the artwork has a block root.
*/
function BylineAvatar({
className,
render,
...props
}: useRender.ComponentProps<"span", BylineAvatarState>) {
return useRender({
defaultTagName: "span",
render,
state: { slot: "byline-avatar" },
props: mergeProps<"span">(
{ className: cn("row-span-2 flex", className) },
props
),
})
}
/** The person's name, 16px/24px semibold, held close to the line below; 12px regular in a `sm` byline. */
function BylineName({ className, ...props }: ComponentProps<"span">) {
return (
<span
data-slot="byline-name"
className={cn(
"self-end text-base/6 font-semibold group-data-[size=sm]/byline:text-xs/5 group-data-[size=sm]/byline:font-normal",
className
)}
{...props}
/>
)
}
/**
* The second line under the name, such as a job title, a company, a handle
* or a date, in 14px/20px muted text; 12px in a `sm` byline.
*/
function BylineDescription({ className, ...props }: ComponentProps<"span">) {
return (
<span
data-slot="byline-description"
className={cn("self-start text-muted-foreground", className)}
{...props}
/>
)
}
export { Byline, BylineAvatar, BylineName, BylineDescription }
import {
Byline,
BylineAvatar,
BylineDescription,
BylineName,
} from "@/components/byline/byline"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
// The copy: change text here, not in the markup.
const PEOPLE = [
{ name: "Mae Jemison", initials: "MJ", role: "Head of Support, Zaira" },
{ name: "Sally Ride", initials: "SR", role: "Founder, Isidora" },
] as const
export default function BylineDemo() {
return (
<div className="grid gap-8 p-6">
{PEOPLE.map((person) => (
<Byline key={person.name}>
<BylineAvatar>
<Avatar size="lg">
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
</BylineAvatar>
<BylineName>{person.name}</BylineName>
<BylineDescription>{person.role}</BylineDescription>
</Byline>
))}
</div>
)
}
The example also uses avatar. Install it too:
pnpm dlx shadcn add @01orbital/avatarnpx shadcn add @01orbital/avataryarn shadcn add @01orbital/avatarbunx --bun shadcn add @01orbital/avatarUsage
import {
Byline,
BylineAvatar,
BylineDescription,
BylineName,
} from "@/components/byline/byline"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
// The copy: change text here, not in the markup.
const PEOPLE = [
{ name: "Mae Jemison", initials: "MJ", role: "Head of Support, Zaira" },
{ name: "Sally Ride", initials: "SR", role: "Founder, Isidora" },
] as const
export default function BylineDemo() {
return (
<div className="grid gap-8 p-6">
{PEOPLE.map((person) => (
<Byline key={person.name}>
<BylineAvatar>
<Avatar size="lg">
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
</BylineAvatar>
<BylineName>{person.name}</BylineName>
<BylineDescription>{person.role}</BylineDescription>
</Byline>
))}
</div>
)
}Anatomy
Byline is built from 4 parts. In the example they nest like this:
Byline
├─ BylineAvatar
├─ BylineName
└─ BylineDescriptionByline: A person's byline: an optionalBylineAvatar, thenBylineNamewith aBylineDescriptionunder it, as adiv. With an avatar it becomes two columns: the avatar spans both lines, 16px from the text. Under a quote, passrender={<figcaption />}; in a centered quote addw-fit text-leftso it shrinks to fit and keeps its text left-aligned.BylineAvatar: Holds the person's picture beside the name and description: a stockAvatar, an orb avatar or an image, 40px recommended. Userender={<div />}when the artwork has a block root.BylineName: The person's name, 16px/24px semibold, held close to the line below; 12px regular in asmbyline.BylineDescription: The second line under the name, such as a job title, a company, a handle or a date, in 14px/20px muted text; 12px in asmbyline.
API reference
Byline
A person's byline: an optional BylineAvatar, then BylineName with a BylineDescription under it, as a div. With an avatar it becomes two columns: the avatar spans both lines, 16px from the text. Under a quote, pass render={<figcaption />}; in a centered quote add w-fit text-left so it shrinks to fit and keeps its text left-aligned.
At size="md" (the default) the name is 16px/24px semibold and the description 14px/20px muted, 2px apart; a 40px avatar matches the two lines. At size="sm" both lines are 12px/20px, the name in regular weight, the byline of a card footer.
Prop
Type
It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
BylineAvatar
Holds the person's picture beside the name and description: a stock Avatar, an orb avatar or an image, 40px recommended. Use render={<div />} when the artwork has a block root.
It accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
BylineName
The person's name, 16px/24px semibold, held close to the line below; 12px regular in a sm byline.
It accepts the HTML attributes of the element it renders.
BylineDescription
The second line under the name, such as a job title, a company, a handle or a date, in 14px/20px muted text; 12px in a sm byline.
It accepts the HTML attributes of the element it renders.
Dependencies
npm: @base-ui/react, cn. The CLI installs them for you.
No other registry pieces.