← All blocks

Profile

Social

Avatar, stats, a follow button, and tabbed content.

Install this section
npx shadcn@latest add https://pouf.worksonmy.dev/r/profile.json

Lands with 9 components: avatar, button, disclosure, icon, layout, media, readout, surface, text.

Live preview — it works, try itsection
AL

Ada Lovelace

@ada
Active now
Posts128
Followers4,201
Following312
Shipped the new cushion buttons
12
1
Optical centering is a rabbit hole
24
2
Pastel that passes WCAG — a thread
36
3
View source
import { useState } from 'react'
import { Card } from '../surface'
import { Stack, Row, Grid } from '../layout'
import { Heading, Text, Eyebrow } from '../text'
import { Avatar } from '../avatar'
import { Badge, Dot } from '../media'
import { Metric } from '../readout'
import { Button } from '../Button'
import { Tabs } from '../disclosure'
import { Icon } from '../Icon'

/** An example profile page: header with avatar and stats, a follow button
 * that moves the follower count, and tabbed content. */
export function ProfileBlock() {
  const [tab, setTab] = useState('posts')
  const [following, setFollowing] = useState(false)
  // Derived, not separate state: following IS the +1 follower, so there's
  // nothing to keep in sync — unfollowing can't leave the count stale.
  const followers = 4201 + (following ? 1 : 0)

  const posts = (
    <Stack gap={3}>
      {['Shipped the new cushion buttons', 'Optical centering is a rabbit hole', 'Pastel that passes WCAG — a thread'].map((p, i) => (
        <Card variant="tight" key={i}>
          <Stack gap={2}>
            <Text>{p}</Text>
            <Row gap={4}>
              <Row gap={1} wrap={false}><Icon name="heart" size="sm" /><Text size="sm" muted>{12 * (i + 1)}</Text></Row>
              <Row gap={1} wrap={false}><Icon name="comment" size="sm" /><Text size="sm" muted>{i + 1}</Text></Row>
            </Row>
          </Stack>
        </Card>
      ))}
    </Stack>
  )

  const about = (
    <Stack gap={3}>
      <Text>Designer & engineer. Making software that feels like it wants to be touched. Currently building Pouf.</Text>
      <Row gap={2}>
        <Badge tone="mint">Design systems</Badge>
        <Badge tone="blue">React</Badge>
        <Badge tone="pink">Motion</Badge>
      </Row>
    </Stack>
  )

  return (
    <div style={{ maxWidth: 640, margin: '0 auto', padding: 24 }}>
      <Card>
        <Stack gap={5}>
          <Row justify="between" align="top">
            <Row gap={3} wrap={false} align="top">
              <Avatar fallback="AL" size="lg" tone="purple" />
              <Stack gap={1}>
                <Heading level={2}>Ada Lovelace</Heading>
                <Eyebrow>@ada</Eyebrow>
                <Row gap={2} wrap={false}>
                  <Dot tone="mint" />
                  <Text size="sm" muted>Active now</Text>
                </Row>
              </Stack>
            </Row>
            <Button tone={following ? 'mint' : 'purple'} variant={following ? 'quiet' : 'solid'} onClick={() => setFollowing((f) => !f)}>
              {following ? 'Following' : 'Follow'}
            </Button>
          </Row>

          <Grid cols={3}>
            <Metric label="Posts" value="128" />
            <Metric label="Followers" value={followers.toLocaleString()} />
            <Metric label="Following" value="312" />
          </Grid>

          <Tabs
            value={tab}
            onChange={setTab}
            tabs={[
              { value: 'posts', label: 'Posts', content: posts },
              { value: 'about', label: 'About', content: about },
            ]}
          />
        </Stack>
      </Card>
    </div>
  )
}

More social blocks

Social feed →