ServicesHow it worksArticles
v1 — Work in progress

Design System

The visual language, component library, and design tokens for UpForService. Built for consistency, accessibility, and scale.

Download tokens JSON

On this page

  • Introduction
  • Principles
  • Colors
  • Typography
  • Spacing
  • Border & Radius
  • Shadows
  • Z-index
  • Breakpoints
  • Components
  • ↳ Button
  • ↳ Badge
  • ↳ Card
  • ↳ Avatar
  • ↳ Text field
  • ↳ Select
  • ↳ Textarea
  • ↳ Separator
  • ↳ Skeleton
  • ↳ Checkbox
  • ↳ Switch
  • ↳ Tabs
  • ↳ Tooltip
  • ↳ Dialog
  • ↳ Callout
  • ↳ Pagination
  • ↳ Breadcrumb

Introduction

The UpForService Design System is the single source of truth for the product's visual language. It provides designers and engineers with shared tokens, patterns, and guidelines to build consistent, accessible interfaces at scale.

Tokens first

Every color, size, and shadow is a named token. No magic values in components.

Composable

Primitives compose into patterns. Patterns compose into pages.

Accessible

WCAG AA contrast, keyboard navigation, and semantic HTML throughout.

Design Principles

Four principles guide every decision in the UpForService design system.

01

Trust through clarity

Financial transactions and service contracts require unambiguous communication. Every interface element must be clear about what it does and what will happen next.

02

Simple in UI, rich in domain

The UI exposes the happy path. The domain model carries versioning, event history, and state machines from day one. Complexity lives in the right layer.

03

Accessible by default

Contrast, keyboard navigation, focus rings, and semantic HTML are not optional features. They are baseline requirements for every component.

04

Internationalisation ready

Every string is a translation key. Layouts account for text expansion. Currency and number formats follow locale conventions.

Colors

All colors are defined as CSS custom properties in globals.css and mapped to Tailwind via @theme inline. The palette supports light and dark themes.

Brand

Primary #0A6FFF: 3.0:1 on white (AA-Large) · use --primary-text for body text

Primary

--primary

#0A6FFF

Primary hover

--primary-hover

#0858D4

Primary text

--primary-text

#0858D4

Tertiary

--tertiary

#7C3AED

Background & Surface

Background

--background

#FFFFFF

Pale blue

--surface-pale-blue

#F8FAFF

Tint blue

--surface-tint-blue

#F0F5FF

Cool

--surface-cool

#F8FAFC

Navy

--surface-navy

#0A1628

Navy mid

--surface-navy-mid

#0D1F3C

Text

All text tokens: 4.5:1+ AA on white background

Heading

--heading

#111827

Foreground

--foreground

#1F2937

Text secondary

--text-secondary

#4B5563

Muted

--muted-foreground

#6B7280

Quiet

--quiet

#9CA3AF

UI Chrome

Card

--card

#FFFFFF

Surface secondary

--secondary

#F8FAFC

Accent

--accent

#F0F5FF

Border

--border

#E5E7EB

Input

--input

#E5E7EB

Muted surface

--muted

#F8FAFF

Status

All status foreground tokens meet 4.5:1 AA on their respective bg tokens

Destructive

--destructive

#EF4444

Warning

--warning

#D97706

Success

--success

#059669

Info

--info

#2563EB

Warning bg

--warning-bg

#FFFBEB

Success bg

--success-bg

#ECFDF5

Disabled

Disabled

--disabled

#E5E7EB

Disabled fg

--disabled-foreground

#9CA3AF

Dark mode

All tokens have dark overrides in the .dark class in globals.css. The dark theme uses the same navy palette as the navy marketing sections, maintaining brand consistency across modes.

Typography

Three font families are loaded via Next.js font optimization. Bricolage Grotesque for display headings, Inter for body and UI, and JetBrains Mono for code and monetary values.

Font families

Display

Aa Bb Cc

Bricolage Grotesque

--font-display

Headings, marketing

Sans

Aa Bb Cc

Inter

--font-sans

Body, UI labels

Mono

Aa Bb Cc

JetBrains Mono

--font-mono

Code, reference IDs

Type scale

Display XL

48–72px · 800 · display

Marketplace

Display LG

36–48px · 800 · display

Find your expert

Display MD

30–36px · 700 · display

Protected payment

H1

36px · 700 · display

Page title

H2

30px · 700 · display

Section heading

H3

24px · 600 · display

Sub-section

H4

20px · 600 · sans

Card heading

Body large

18px · 400 · sans

The marketplace for structured professional services.

Body

16px · 400 · sans

Clear scope, protected payment, public reputation.

Body small

14px · 400 · sans

Your payment is held in escrow until the service is complete.

Caption

12px · 400 · sans

Last updated 3 minutes ago · Ref #UFS-20240115

Label

14px · 500 · sans

Service category

Mono

14px · 400 · mono

7eTp3xKmN9...8sQwZ · $1,234.56

Spacing

4px base grid. All spacing values are multiples of 4px. Use Tailwind's spacing scale directly — p-4 = 16px, gap-6 = 24px.

TokenRemVisual
space-0
0rem
0px
space-1
0.25rem
4px
space-2
0.5rem
8px
space-3
0.75rem
12px
space-4
1rem
16px
space-5
1.25rem
20px
space-6
1.5rem
24px
space-8
2rem
32px
space-10
2.5rem
40px
space-12
3rem
48px
space-16
4rem
64px
space-20
5rem
80px
space-24
6rem
96px
space-32
8rem
128px
space-40
10rem
160px
space-48
12rem
192px
space-64
16rem
256px

Border & Radius

Radius tokens are defined as CSS custom properties and mapped to Tailwind via @theme inline. Use rounded-pill, rounded-card, rounded-md, rounded-sm, rounded-xs.

None

--radius-none

0px

xs

--radius-xs

6px

sm

--radius-sm

10px

md

--radius-md

16px

Card / lg

--radius-card

24px

Pill / full

--radius-pill

9999px

Border widths

border

1px

border-2

2px

border-4

4px

Shadows

Shadows are named semantically. Use shadow-raised for elevated UI elements, shadow-feature-panel for cards in marketing sections, and shadow-cta-glow for primary action buttons.

raised

--shadow-raised

feature-panel

--shadow-feature-panel

marketing

--shadow-marketing

cta-glow

--shadow-cta-glow

Focus ring

All interactive elements use --ring for focus visibility. Applied via focus-visible:ring-[3px] focus-visible:ring-ring/50.

Z-index scale

Defined in the tokens JSON. Use these values to maintain consistent stacking order across the application.

LayerValueUsage
base0Default document flow
raised10Slightly elevated cards, sticky headers within flow
dropdown1000Menus, comboboxes, select dropdowns
sticky1100Sticky navigation bar, filters sidebar
fixed1200Fixed banners, announcement bars
modal1300Dialog overlays
popover1400Popovers, date pickers, command palette
tooltip1500Tooltips
toast1600Toast notifications (Sonner)

Breakpoints & Grid

Mobile-first responsive design. Tailwind's default breakpoint scale with a max-w-[1200px] content container and max-w-[1400px] for full-bleed navigation.

PrefixMin-widthTypical context
—0pxMobile portrait (base, no prefix needed)
sm:640pxMobile landscape, small tablets
md:768pxTablet portrait
lg:1024pxLaptop / desktop
xl:1280pxWide desktop
2xl:1536pxUltra-wide

Container & gutters

Content max-width

1200px

Pages, sections

Nav max-width

1400px

Top nav, footer

Gutter (mobile)

px-4 (16px)

All viewports

Gutter (md+)

px-6 (24px)

Tablet and up

Grid columns

12

CSS Grid base

Components

Built on shadcn/ui primitives with Radix UI. All components use class-variance-authority for variants and accept a className prop for extension.

Button

Primary interactive element. Uses rounded-pill by default. Supports asChild for rendering as a link via Slot.

Variants

Sizes

States

import { Button } from '@/components/ui/button'
import Link from 'next/link'

// Default
<Button>Get started</Button>

// Variant + size
<Button variant="outline" size="sm">Cancel</Button>
<Button variant="destructive">Delete offer</Button>

// As Next.js link (asChild)
<Button asChild>
  <Link href="/dashboard">Go to dashboard</Link>
</Button>

// Icon button – always include aria-label
<Button size="icon" aria-label="Share profile">
  <Share2 className="size-4" />
</Button>
Accessibility: Always use a descriptive aria-label on icon-only buttons. Disabled buttons use aria-disabled and pointer-events-none.

Badge

Inline label for status, categories, and metadata. Uses rounded-full and supports asChild.

Variants

DefaultSecondaryOutlineGhostDestructiveLinkEyebrow — category label

Status examples

ConfirmedPendingFailedIn progress
import { Badge } from '@/components/ui/badge'

<Badge>Active</Badge>
<Badge variant="destructive">Failed</Badge>
<Badge variant="outline">Pending</Badge>
<Badge variant="secondary">Draft</Badge>

// Status with icon
<Badge className="bg-success text-success-foreground">
  <CheckCircle2 className="size-3" />
  Confirmed
</Badge>

// Eyebrow — purely typographic, no visual container
<Badge variant="eyebrow">Service category</Badge>

Card

Container for grouped content. Uses rounded-card (24px). Sub-components: CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter.

Default

Service offer
React performance audit and optimisation
Active

Detailed review of your React app's render cycles, bundle size, and Core Web Vitals.

Feature (elevated)

Payment protected
Escrow holds your payment until delivery

Funds are released only after you confirm the service was delivered as agreed.

import { Card, CardHeader, CardTitle, CardDescription,
  CardAction, CardContent, CardFooter } from '@/components/ui/card'

// Default
<Card>
  <CardHeader>
    <CardTitle>Service offer</CardTitle>
    <CardDescription>React performance audit</CardDescription>
    <CardAction><Badge>Active</Badge></CardAction>
  </CardHeader>
  <CardContent>Description goes here.</CardContent>
  <CardFooter><Button>View details</Button></CardFooter>
</Card>

// Feature (elevated shadow)
<Card variant="feature">...</Card>

Avatar

User representation with image, initials fallback, and badge support. Built on Radix UI Avatar primitive.

Sizes

SMsm
MDmd
LGlg
LBimage

Avatar group

ABCDEF
+12

Deterministic color backgrounds

ABCDEFGHIJKLMNOP

8 backgrounds at OKLCH L=0.45 C=0.10 — all pass 4.5:1 WCAG AA against white initials.

import { Avatar, AvatarImage, AvatarFallback,
  AvatarGroup, AvatarGroupCount } from '@/components/ui/avatar'

// With image + fallback
<Avatar size="lg">
  <AvatarImage src="/photo.jpg" alt="Lucy Brandão" />
  <AvatarFallback
    style={{ backgroundColor: 'var(--avatar-bg-3)', color: 'var(--avatar-fg)' }}>
    LB
  </AvatarFallback>
</Avatar>

// Group
<AvatarGroup>
  <Avatar><AvatarFallback>AB</AvatarFallback></Avatar>
  <Avatar><AvatarFallback>CD</AvatarFallback></Avatar>
  <AvatarGroupCount>+5</AvatarGroupCount>
</AvatarGroup>

Text field

Single-line text input. Shares focus, error, and disabled tokens consistently across all form controls.

Default

With helper text

Visible on your public profile page.

Error state

Enter a valid email address.

Disabled

Address is locked after verification.

import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

// Default
<div className="space-y-2">
  <Label htmlFor="title">Service title</Label>
  <Input id="title" placeholder="React performance audit" />
</div>

// Error state
<div className="space-y-2">
  <Label htmlFor="email" className="text-destructive">Email</Label>
  <Input id="email" aria-invalid="true"
    className="border-destructive focus-visible:ring-destructive/20" />
  <p className="text-xs text-destructive" role="alert">
    Enter a valid email address.
  </p>
</div>

// Disabled
<Input disabled value="locked@value.com" />
Accessibility: Every input must have an associated Label (via htmlFor) or an aria-label. Error messages use aria-invalid="true".

Select

Dropdown selection built on Radix UI Select. Keyboard navigable, supports groups, labels, and disabled items.

Default

With groups

Error state

Enter a valid email address.

Disabled

import { Select, SelectTrigger, SelectValue, SelectContent,
  SelectItem, SelectGroup, SelectLabel, SelectSeparator
} from '@/components/ui/select'

<Select>
  <SelectTrigger className="w-full">
    <SelectValue placeholder="Select category" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Technology</SelectLabel>
      <SelectItem value="web">Web development</SelectItem>
      <SelectItem value="mobile">Mobile apps</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectLabel>Creative</SelectLabel>
      <SelectItem value="design">Graphic design</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>
Accessibility: Arrow keys navigate options, Enter/Space selects. Use SelectLabel and SelectSeparator for grouped options. Disabled items use data-[disabled].

Textarea

Multi-line text input. Uses field-sizing-content for automatic height growth. Shares the same focus, error, and disabled tokens as Text field.

Default

Maximum 2 000 characters.

With helper text

Your review will be public on the provider's profile.

Error state

Bio must be at least 20 characters.

Disabled

Internal notes are only visible to you.

import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'

<div className="space-y-2">
  <Label htmlFor="desc">Service description</Label>
  <Textarea id="desc"
    placeholder="Describe what's included…"
  />
  <p className="text-xs text-muted-foreground">
    Maximum 2 000 characters.
  </p>
</div>
Accessibility: Always pair with Label. Error state uses aria-invalid="true". Auto-resize via field-sizing-content requires no JavaScript.

Separator

Thin visual divider. Horizontal by default; use orientation="vertical" for inline contexts.

Horizontal

Section A

Section B

Section C

Vertical

Overview
Reviews
Questions
import { Separator } from '@/components/ui/separator'

// Horizontal (default) — between sections
<Separator />

// Vertical — between inline items
<div className="flex items-center gap-4 h-8">
  <span>Overview</span>
  <Separator orientation="vertical" />
  <span>Reviews</span>
</div>

// Semantic (not decorative)
<Separator decorative={false} aria-label="Between navigation regions" />
Accessibility: Pass decorative={false} and an aria-label when the separator conveys structure (e.g. between navigation regions).

Skeleton

Animated placeholder shown while content is loading. Use className to match the shape of the real content.

Text lines

Card

Avatar + text

import { Skeleton } from '@/components/ui/skeleton'

// Text lines
<div className="space-y-2">
  <Skeleton className="h-4 w-full" />
  <Skeleton className="h-4 w-5/6" />
</div>

// Avatar + text
<div className="flex items-center gap-3">
  <Skeleton className="size-10 rounded-full" />
  <div className="space-y-2">
    <Skeleton className="h-4 w-32" />
    <Skeleton className="h-3 w-20" />
  </div>
</div>

// Wrap loading region for screen readers
<div aria-busy="true" aria-label="Loading offers">
  <Skeleton className="h-48 w-full rounded-card" />
</div>
Accessibility: Wrap loading regions in aria-busy="true" and remove it once the real content appears. Skeleton itself carries no ARIA role.

Checkbox

Binary input for boolean selections. Built on Radix UI Checkbox — fully keyboard accessible.

States

import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'

// Default
<div className="flex items-center gap-2">
  <Checkbox id="terms" />
  <Label htmlFor="terms">Accept terms of service</Label>
</div>

// Controlled
const [checked, setChecked] = useState(false)
<Checkbox checked={checked} onCheckedChange={setChecked} />

// Indeterminate (e.g. select-all with partial selection)
<Checkbox checked="indeterminate" />
Accessibility: Always pair with a Label via htmlFor. The indeterminate state uses checked="indeterminate" and requires a programmatic trigger — it is not a native HTML state.

Switch

Toggle for on/off states. Prefer Switch over Checkbox when the action takes effect immediately (no form submit needed).

Default (sm + default sizes)

States

import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'

// Uncontrolled
<div className="flex items-center gap-2">
  <Switch id="notifications" />
  <Label htmlFor="notifications">Email notifications</Label>
</div>

// Controlled
const [on, setOn] = useState(false)
<Switch checked={on} onCheckedChange={setOn} />

// Small size
<Switch size="sm" />
Accessibility: Always pair with a Label. The Switch communicates its state via aria-checked — no extra ARIA needed.

Tabs

Organises related content into selectable panels. Supports two visual variants: default (pill) and line.

Default (pill)

Full service description, inclusions, process, and estimated timeline appear here.

Line

Full service description, inclusions, process, and estimated timeline appear here.

import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'

// Default (pill) variant
<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="reviews">Reviews</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">Content A</TabsContent>
  <TabsContent value="reviews">Content B</TabsContent>
</Tabs>

// Line variant
<TabsList variant="line">
  <TabsTrigger value="overview">Overview</TabsTrigger>
</TabsList>
Accessibility: Keyboard: arrow keys move focus between tabs, Enter/Space activates. Tab panels are linked via aria-controls / aria-labelledby by Radix.

Tooltip

Non-interactive contextual label triggered on hover/focus. Wrap interactive elements in TooltipProvider at the root of the tree.

Default

import { Tooltip, TooltipTrigger, TooltipContent,
  TooltipProvider } from '@/components/ui/tooltip'

// Wrap the tree once (typically in a layout)
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild>
      <Button variant="ghost" size="icon" aria-label="Escrow info">
        <ShieldCheck className="size-4" />
      </Button>
    </TooltipTrigger>
    <TooltipContent>
      Funds held in escrow until delivery is confirmed
    </TooltipContent>
  </Tooltip>
</TooltipProvider>
Accessibility: Tooltip content must not be the only source of critical information — it is invisible to keyboard users who do not focus the trigger. Use TooltipProvider delayDuration={0} to remove open delay.

Dialog

Modal overlay that focuses attention on a critical decision or form. Built on Radix UI Dialog — traps focus, closes on Escape.

Default

Destructive action

import { Dialog, DialogTrigger, DialogContent, DialogHeader,
  DialogTitle, DialogDescription, DialogFooter
} from '@/components/ui/dialog'

<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline">Open dialog</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Confirm order</DialogTitle>
      <DialogDescription>
        Payment will be held in escrow until delivery.
      </DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <Button variant="outline">Cancel</Button>
      <Button>Place order</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
Accessibility: DialogTitle and DialogDescription are required — they provide the accessible name and description consumed by aria-labelledby / aria-describedby on the dialog container.

Callout

Inline contextual message with icon. Used to surface trust cues, status notes, and non-blocking alerts without interrupting the user's flow.

Info (default)

Your payment is held in escrow until delivery is confirmed. Adjustments and refunds follow the Order Term.

Success

Identity verified. Your profile is active and visible to buyers.

Warning

This offer will be automatically paused after 30 days of inactivity.

Danger

Payment failed. The transaction was not confirmed within the timeout window.

Neutral

Availability shown is indicative. Confirm timing directly with the provider.
import { Callout } from '@/components/ui/callout'
import { ShieldCheck, AlertTriangle } from 'lucide-react'

// Info (default) — escrow trust cue
<Callout icon={<ShieldCheck />}>
  Your payment is protected by escrow until delivery.
</Callout>

// Warning
<Callout variant="warning" icon={<AlertTriangle />}>
  This offer will be paused after 30 days of inactivity.
</Callout>

// Critical — add role="alert" for screen readers
<Callout variant="danger" icon={<AlertCircle />} role="alert">
  Payment failed. Please try again.
</Callout>
Accessibility: Callout is a presentational container — it carries no ARIA role by default. If the message is time-sensitive or critical, wrap it in a role="alert" element so screen readers announce it immediately.

Pagination

Offset-based pagination with truncation for large page counts. Page 1 omits the page query param. The live component uses useSearchParams for URL-driven state — import from @/components/public/pagination.

Default (page 2 of 8)

…

First page (prev disabled)

Last page (next disabled)

import { Pagination } from '@/components/public/pagination'

// URL-driven — reads/writes ?page= search param
<Suspense>
  <Pagination total={180} pageSize={20} page={currentPage} />
</Suspense>

Breadcrumb

Hierarchical navigation trail showing the user's location within the product. Built with semantic nav + ol and aria-current="page" on the active item.

Default

  1. Home
  2. Services
  3. React Performance Audit

With ellipsis

  1. Home
  2. More
  3. Technology
  4. React Performance Audit

Custom separator

  1. Home
  2. /
  3. Services
  4. /
  5. React Performance Audit
import { Breadcrumb, BreadcrumbList, BreadcrumbItem,
  BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator,
  BreadcrumbEllipsis } from '@/components/ui/breadcrumb'
import Link from 'next/link'

<Breadcrumb>
  <BreadcrumbList>
    <BreadcrumbItem>
      <BreadcrumbLink asChild>
        <Link href="/">Home</Link>
      </BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbLink asChild>
        <Link href="/buscar">Services</Link>
      </BreadcrumbLink>
    </BreadcrumbItem>
    <BreadcrumbSeparator />
    <BreadcrumbItem>
      <BreadcrumbPage>React Performance Audit</BreadcrumbPage>
    </BreadcrumbItem>
  </BreadcrumbList>
</Breadcrumb>
Accessibility: The Breadcrumb root renders aria-label="Breadcrumb". The current page uses aria-current="page". Separators are aria-hidden. Use BreadcrumbLink asChild with Next.js Link for client-side navigation.

Design System v1

This is a living document. New components and tokens will be added as the product evolves. Download the tokens to use them in Figma or other tools.

Download tokens JSONBack to marketplace

UpForService connects buyers and professionals through structured offers, public reputation, and protected payment.

Marketplace

Search servicesCategoriesHow it works

Platform

Protected paymentSecurity

Company

AboutTerms of servicePrivacyCookiesHelpCareersDesign System

Operator: UpForService (razão social a definir) · Company ID (CNPJ) CNPJ a definir

Contact: contato a definir · Data Protection Officer: encarregado a definir

© 2026 UpForService. International services marketplace.