Section
A block of text with one accessory beside it — a thumbnail, or a button.
A block of text with one accessory beside it — a thumbnail, or a button. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
import {
Section,
SectionAccessory,
SectionContent,
SectionHeading,
SectionThumbnail,
} from "@/registry/bases/newt/ui/section"
/*
* Drawn as a data URI rather than fetched: a demo that needs the network is a
* demo that fails offline, and the thumbnail only has to be a thumbnail.
*/
const COVER =
"data:image/svg+xml;utf8," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5865f2"/><stop offset="1" stop-color="#eb459e"/>
</linearGradient></defs>
<rect width="86" height="86" fill="url(#g)"/>
</svg>`
)
export default function SectionDemo() {
return (
<Section className="max-w-[520px]">
<SectionContent>
<SectionHeading>Release 7.3</SectionHeading>
<p>
The update is out. Chests open on the first try, the servers hold at
peak hours, and gravity only changes on Thursdays.
</p>
</SectionContent>
<SectionAccessory>
<SectionThumbnail src={COVER} alt="Release artwork" />
</SectionAccessory>
</Section>
)
}<script setup lang="ts">
import {
Section,
SectionAccessory,
SectionContent,
SectionHeading,
SectionThumbnail,
} from "~~/registry/bases/newt/ui/section"
/*
* Drawn as a data URI rather than fetched: a demo that needs the network is a
* demo that fails offline, and the thumbnail only has to be a thumbnail.
*/
const cover =
"data:image/svg+xml;utf8," +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5865f2"/><stop offset="1" stop-color="#eb459e"/>
</linearGradient></defs>
<rect width="86" height="86" fill="url(#g)"/>
</svg>`
)
</script>
<template>
<Section class="max-w-[520px]">
<SectionContent>
<SectionHeading>Release 7.3</SectionHeading>
<p>
The update is out. Chests open on the first try, the servers hold at
peak hours, and gravity only changes on Thursdays.
</p>
</SectionContent>
<SectionAccessory>
<SectionThumbnail :src="cover" alt="Release artwork" />
</SectionAccessory>
</Section>
</template>Installation
pnpm dlx newtui@latest add sectionnpx newtui@latest add sectionyarn dlx newtui@latest add sectionbunx newtui@latest add sectionCopy the source into your project
import * as React from "react"
import { cn } from "@/lib/utils"
/*
* A block of text with one thing beside it: a thumbnail, or a button. That
* pairing is the whole component — the text column takes the space that is
* left, and the accessory keeps its own width whatever the text does.
*/
const Section = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex w-full items-start gap-4", className)}
{...props}
/>
))
Section.displayName = "Section"
const SectionContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"flex min-w-0 flex-1 flex-col gap-2 text-[15px] leading-[1.375] text-newt-text-secondary",
className
)}
{...props}
/>
))
SectionContent.displayName = "SectionContent"
const SectionHeading = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn(
"font-display text-xl font-bold leading-tight text-newt-text-primary",
className
)}
{...props}
/>
))
SectionHeading.displayName = "SectionHeading"
/*
* The accessory never shrinks and never stretches: it is the fixed side of
* the pair, which is what keeps a long paragraph from squeezing a thumbnail
* into a sliver.
*/
const SectionAccessory = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex shrink-0 items-start", className)}
{...props}
/>
))
SectionAccessory.displayName = "SectionAccessory"
export interface SectionThumbnailProps extends React.ImgHTMLAttributes<HTMLImageElement> {
/**
* Marks the image as a spoiler: blurred until clicked, the same treatment
* a spoilered attachment gets.
*/
spoiler?: boolean
}
const SectionThumbnail = React.forwardRef<
HTMLImageElement,
SectionThumbnailProps
>(({ className, spoiler = false, alt = "", ...props }, ref) => {
const [revealed, setRevealed] = React.useState(false)
const hidden = spoiler && !revealed
return (
<img
ref={ref}
alt={alt}
data-spoiler={hidden || undefined}
/*
* A spoilered thumbnail is a button until it is opened: it takes a
* click and a key, and says what it is to a screen reader.
*/
role={hidden ? "button" : undefined}
tabIndex={hidden ? 0 : undefined}
aria-label={hidden ? "Spoiler, click to reveal" : undefined}
onClick={() => setRevealed(true)}
onKeyDown={(event) => {
if (!hidden) return
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
setRevealed(true)
}
}}
className={cn(
"h-[86px] w-[86px] rounded-lg object-cover",
hidden && "cursor-pointer blur-lg",
className
)}
{...props}
/>
)
})
SectionThumbnail.displayName = "SectionThumbnail"
export {
Section,
SectionAccessory,
SectionContent,
SectionHeading,
SectionThumbnail,
}<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
/*
* A block of text with one thing beside it: a thumbnail, or a button. That
* pairing is the whole component — the text column takes the space that is
* left, and the accessory keeps its own width whatever the text does.
*/
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div :class="cn('flex w-full items-start gap-4', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
/*
* The accessory never shrinks and never stretches: it is the fixed side of
* the pair, which is what keeps a long paragraph from squeezing a thumbnail
* into a sliver.
*/
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div :class="cn('flex shrink-0 items-start', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
:class="
cn(
'flex min-w-0 flex-1 flex-col gap-2 text-[15px] leading-[1.375] text-newt-text-secondary',
props.class
)
"
>
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<p
:class="
cn(
'font-display text-xl font-bold leading-tight text-newt-text-primary',
props.class
)
"
>
<slot />
</p>
</template><script setup lang="ts">
import { ref, type HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
src: string
alt?: string
/**
* Marks the image as a spoiler: blurred until clicked, the same treatment
* a spoilered attachment gets.
*/
spoiler?: boolean
}>(),
{ alt: "", spoiler: false }
)
const revealed = ref(false)
function reveal() {
revealed.value = true
}
function onKeydown(event: KeyboardEvent) {
if (revealed.value || !props.spoiler) return
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
reveal()
}
}
</script>
<template>
<img
:src="props.src"
:alt="props.alt"
:data-spoiler="props.spoiler && !revealed ? true : undefined"
:role="props.spoiler && !revealed ? 'button' : undefined"
:tabindex="props.spoiler && !revealed ? 0 : undefined"
:aria-label="
props.spoiler && !revealed ? 'Spoiler, click to reveal' : undefined
"
:class="
cn(
'h-[86px] w-[86px] rounded-lg object-cover',
props.spoiler && !revealed && 'cursor-pointer blur-lg',
props.class
)
"
@click="reveal"
@keydown="onKeydown"
/>
</template>export { default as Section } from "./Section.vue"
export { default as SectionAccessory } from "./SectionAccessory.vue"
export { default as SectionContent } from "./SectionContent.vue"
export { default as SectionHeading } from "./SectionHeading.vue"
export { default as SectionThumbnail } from "./SectionThumbnail.vue"Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { Section } from "@/components/ui/section"
export function Example() {
return <Section />
}<script setup lang="ts">
import { Section, SectionAccessory, SectionContent, SectionHeading, SectionThumbnail } from "@/components/ui/section"
</script>
<template>
<Section />
</template>API reference
Section
children / default slot?React.ReactNode / Vue slot
Content rendered inside the component.
React.ReactNode / Vue slotclassName / class?string
Additional classes merged with the component defaults.
stringAccessibility
Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.
Tokens
Section reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer.