Unbounded para títulos
font-display pesos 500, 600
Software que trabalha por você.
Peso 600, tracking de -0,03em a -0,045em, entrelinha de 0,95 a 1,1. Ecoa a geometria larga e redonda do TWO.
Os tokens, os componentes e os padrões que constroem a Two9. Cada exemplo desta página é o componente de produção, renderizado ao vivo.
01
O símbolo da Two9 é um bloco amarelo com um 9: uma tecla. Todo elemento interativo nasce dele, com face, borda inferior visível e curso ao ser pressionado.
02
O amarelo não é tinta espalhada. É luz que vaza por baixo do grafite: em pontos de ação, estados ativos e brilhos. Uma seção inteira amarela só existe como momento deliberado.
03
É a metáfora dos agentes de IA: o trabalho acontece sem alguém apertar. As animações digitam, acendem em sequência e mandam ondas pelas teclas.
04
O raio do bloco é 52 sobre 363 do lado. Todo elemento quadrado repete essa proporção, de uma tecla de 44px a um painel inteiro.
Duas camadas. A paleta crua vira utilitários como bg-charcoal-800. Os papéis semânticos, como bg-surface e text-muted, mudam de valor com o tema. Prefira os papéis: eles fazem o mesmo componente funcionar no site escuro e nas prévias claras.
Carvão
O fundo da marca e as teclas grafite.
key-skirt
Saia da tecla
Lateral escura das teclas grafite, abaixo do topo.
charcoal-950
Carvão 950
Borda inferior das teclas e fundo mais profundo.
charcoal-900
Carvão 900
Seções alternadas, poços e tabuleiros.
charcoal-800
Carvão
Fundo da marca. A maior parte de cada tela.
charcoal-700
Grafite
Teclas e superfícies elevadas.
charcoal-600
Carvão 600
Hover de teclas grafite e camadas sobrepostas.
charcoal-500
Carvão 500
Barras de rolagem e detalhes de interface.
Fumaça
Textos sobre carvão e o fundo do tema claro.
smoke-400
Fumaça 400
Texto apagado e placeholders.
smoke-300
Fumaça 300
Texto secundário sobre carvão.
smoke-200
Fumaça 200
Texto corrido sobre carvão.
smoke-100
Fumaça 100
Fundo profundo no tema claro.
Neutros
Papel, branco e a tinta do 9.
paper
Papel
Fundo do tema claro.
white
Branco
Títulos sobre fundo escuro.
ink
Tinta
O 9 do logo e texto sobre amarelo.
Amarelo
A luz, da borda da tecla ao brilho do texto.
yellow-300
Amarelo 300
Brilho do texto em teclas acesas.
yellow-400
Amarelo 400
Hover do amarelo no tema escuro.
yellow-500
Amarelo Two9
A luz. Ação, destaque e o bloco do logo.
yellow-600
Amarelo 600
Hover do amarelo no tema claro.
yellow-700
Amarelo 700
Borda inferior das teclas amarelas.
yellow-800
Amarelo 800
Amarelo como texto sobre fundo claro.
Estados
Sucesso, erro e informação. Sempre com ícone ou texto junto.
success-500
Sucesso
Confirmações e estados concluídos.
danger-500
Erro
Erros de formulário sobre carvão.
danger-on-accent
Erro sobre amarelo
Mensagens de erro sobre superfícies amarelas.
info-500
Informação
Avisos neutros.
Ative com data-theme="dark" em qualquer elemento.
Superfícies
Fundo vaziocharcoal-950
bg-surface-void / --surface-void
Fundo afundadocharcoal-900
bg-surface-sunken / --surface-sunken
Fundocharcoal-800
bg-surface / --surface-base
Fundo elevadocharcoal-700
bg-surface-raised / --surface-raised
Sobreposiçãocharcoal-600
bg-surface-overlay / --surface-overlay
Texto
Texto fortewhite
text-strong / --text-strong
Texto corridosmoke-200
text-body / --text-body
Texto secundáriosmoke-300
text-muted / --text-muted
Texto apagadosmoke-400
text-faint / --text-faint
Linhas
Linha sutilrgb(255 255 255 / 0.08)
border-line / --line-subtle
Linha fortergb(255 255 255 / 0.16)
border-line-strong / --line-strong
Destaque
Destaqueyellow-500
bg-accent / --accent
Destaque forteyellow-400
bg-accent-strong / --accent-strong
Destaque como textoyellow-500
text-accent-text / --accent-text
Sobre o destaqueink
text-on-accent / --on-accent
Foco
Anel de focoyellow-500
:focus-visible / --focus-ring
Ative com data-theme="light" em qualquer elemento.
Superfícies
Fundo vaziosmoke-100
bg-surface-void / --surface-void
Fundo afundadosmoke-100
bg-surface-sunken / --surface-sunken
Fundopaper
bg-surface / --surface-base
Fundo elevadowhite
bg-surface-raised / --surface-raised
Sobreposiçãowhite
bg-surface-overlay / --surface-overlay
Texto
Texto forteink
text-strong / --text-strong
Texto corrido#3a3638
text-body / --text-body
Texto secundário#5e5b5c
text-muted / --text-muted
Texto apagado#7a7778
text-faint / --text-faint
Linhas
Linha sutilrgb(35 31 32 / 0.1)
border-line / --line-subtle
Linha fortergb(35 31 32 / 0.2)
border-line-strong / --line-strong
Destaque
Destaqueyellow-500
bg-accent / --accent
Destaque forteyellow-600
bg-accent-strong / --accent-strong
Destaque como textoyellow-800
text-accent-text / --accent-text
Sobre o destaqueink
text-on-accent / --on-accent
Foco
Anel de focoink
:focus-visible / --focus-ring
| Texto | Fundo vazio | Fundo afundado | Fundo | Fundo elevado |
|---|---|---|---|---|
| Texto forte | 18,6:1AAA | 17,3:1AAA | 15,6:1AAA | 13,6:1AAA |
| Texto corrido | 11,6:1AAA | 10,8:1AAA | 9,7:1AAA | 8,5:1AAA |
| Texto secundário | 6,9:1AA | 6,4:1AA | 5,8:1AA | 5,1:1AA |
| Texto apagado | 4,2:1AA grande | 3,9:1AA grande | 3,5:1AA grande | 3,0:1AA grande |
| Destaque como texto | 12,7:1AAA | 11,8:1AAA | 10,7:1AAA | 9,3:1AAA |
| Texto | Fundo vazio | Fundo afundado | Fundo | Fundo elevado |
|---|---|---|---|---|
| Texto forte | 13,6:1AAA | 13,6:1AAA | 15,0:1AAA | 16,3:1AAA |
| Texto corrido | 10,0:1AAA | 10,0:1AAA | 11,0:1AAA | 11,9:1AAA |
| Texto secundário | 5,6:1AA | 5,6:1AA | 6,2:1AA | 6,7:1AA |
| Texto apagado | 3,7:1AA grande | 3,7:1AA grande | 4,0:1AA grande | 4,4:1AA grande |
| Destaque como texto | 5,3:1AA | 5,3:1AA | 5,8:1AA | 6,3:1AA |
text-accent-text, que vira amarelo 800. O amarelo 500 sobre papel fica em 1,3:1.Três famílias, cada uma com um trabalho. Unbounded para títulos, Figtree para todo o resto e Martian Mono só para código e registros de agente. Os tamanhos seguem uma escala clássica estendida para display.
Unbounded para títulos
font-display pesos 500, 600
Software que trabalha por você.
Peso 600, tracking de -0,03em a -0,045em, entrelinha de 0,95 a 1,1. Ecoa a geometria larga e redonda do TWO.
Figtree para texto e interface
font-sans pesos 400, 500, 600
A Two9 projeta e desenvolve sites, aplicativos, sistemas e agentes de IA para empresas que querem crescer sem multiplicar a equipe.
Corpo em 18px no desktop e 16px no celular, entrelinha 1,6, linhas de até 70 caracteres.
Martian Mono para código e registros
font-mono pesos 400
agenda.reservar({ horario: "10h30" })
Só para código de verdade e registros de agente. Nunca para rótulos decorativos.
Título de página
<h1>
Design system
font-display text-[clamp(2.6rem,7.4vw,7.5rem)] leading-[0.98] tracking-[-0.045em]Título de seção
<h2>
O que a Two9 constrói
font-display text-[clamp(2.25rem,5vw,4.5rem)] leading-[1.02] tracking-[-0.04em]Título de bloco
<h3>
Agentes de IA
font-display text-xl lg:text-[1.75rem] tracking-[-0.03em]Introdução
<p>
Do agente que atende no WhatsApp ao sistema que organiza a operação.
text-md sm:text-lg text-body max-w-[34rem]Corpo
<p>
Corpo em 18px no desktop e 16px no celular, com entrelinha 1,6 e linhas de até 70 caracteres.
text-base sm:text-md text-body max-w-[40rem]Rótulo e legenda
<span>
Nome completo
text-sm font-semibold text-strong / text-sm text-mutedCódigo e registros
<code>
crm.criar_lead({ origem: "whatsapp" })
font-mono text-[0.8125rem]text-7xl120px, 7,5rem, 0,94Luz de baixotext-6xl96px, 6rem, 0,96Luz de baixotext-5xl72px, 4,5rem, 1Luz de baixotext-4xl60px, 3,75rem, 1,04Luz de baixotext-3xl48px, 3rem, 1,08Luz de baixotext-2xl36px, 2,25rem, 1,15Luz de baixotext-xl24px, 1,5rem, 1,35Luz de baixotext-lg21px, 1,3125rem, 1,5Luz de baixotext-md18px, 1,125rem, 1,6Luz de baixotext-base16px, 1rem, 1,6Luz de baixotext-sm14px, 0,875rem, 1,55Luz de baixotext-xs12px, 0,75rem, 1,5Luz de baixoA base é 4px, a escala padrão do Tailwind. O número da classe vezes 4 dá a medida: p-6 é 24px e gap-2.5 é 10px. Use só os degraus abaixo.
0,52pxAjustes ópticos.14pxEntre ícone e texto pequeno.1,56pxÍcone de mensagem e texto.28pxRótulo e campo.2,510pxÍcone e texto em botões, entre chips.312pxEntre teclas pequenas.416pxPadding de botão pequeno.520pxMargem lateral no celular.624pxPadding de botão, entre blocos de texto.832pxMargem lateral a partir de 640px, padding de botão grande.1040pxEntre título e conteúdo.1248pxEntre colunas do grid.1664pxEntre grupos dentro de uma seção.2080pxEntre o cabeçalho de seção e o conteúdo.2496pxPadding vertical de seção no celular.32128pxPadding vertical de seção a partir de 640px.40160pxPadding vertical de seção a partir de 1024px.96px
Celular, py-24
128px
A partir de 640px, sm:py-32
160px
A partir de 1024px, lg:py-40
Todo canto vem do bloco do logo: 52 de raio para 363 de lado, 14,3%. Elementos quadrados calculam o raio com TILE_RADIUS_RATIO. O resto usa a escala de rounded-xs a rounded-tile.
rounded-xs
4px, 0,25rem
Anel de foco, selos.
rounded-sm
8px, 0,5rem
Botões pequenos e médios, teclas de 56px.
rounded-md
12px, 0,75rem
Botão grande, chips, campos.
rounded-lg
20px, 1,25rem
Painéis e prévias.
rounded-xl
28px, 1,75rem
Teclas de serviço e cartões grandes.
rounded-2xl
40px, 2,5rem
Painéis de seção, como o contato.
rounded-tile
52px, 3,25rem
O bloco do logo com 363px de lado.
raio = lado × 52 / 363
Não há sombras genéricas. Cada sombra desenha uma tecla: brilho na aresta de cima, a borda sólida embaixo e a sombra no fundo. Pressionar troca shadow-key por shadow-key-pressed e desce 5px.
Tecla
shadow-key
Teclas grafite em repouso: botão secundário, chips, keycaps.
0 1px 0 0 rgb(255 255 255 / 0.06) inset0 -2px 0 0 rgb(0 0 0 / 0.35) inset0 6px 0 0 #1412130 14px 28px -10px rgb(0 0 0 / 0.7)Tecla pressionada
shadow-key-pressed
A mesma tecla afundada 5px: a borda some e a sombra encolhe.
0 1px 0 0 rgb(255 255 255 / 0.04) inset0 -1px 0 0 rgb(0 0 0 / 0.3) inset0 1px 0 0 #1412130 4px 10px -6px rgb(0 0 0 / 0.7)Tecla amarela
shadow-key-yellow
Botão primário e teclas acesas: borda em amarelo 700 e luz amarela por baixo.
0 1px 0 0 rgb(255 255 255 / 0.45) inset0 -2px 0 0 rgb(0 0 0 / 0.18) inset0 6px 0 0 #a08c000 18px 40px -12px rgb(242 214 17 / 0.45)Tecla amarela pressionada
shadow-key-yellow-pressed
Botão primário durante o clique.
0 1px 0 0 rgb(255 255 255 / 0.35) inset0 -1px 0 0 rgb(0 0 0 / 0.15) inset0 1px 0 0 #a08c000 6px 18px -8px rgb(242 214 17 / 0.5)Brilho
shadow-glow
Estados ativos e selecionados sem borda de tecla.
0 0 0 1px rgb(242 214 17 / 0.4)0 0 32px 4px rgb(242 214 17 / 0.25)Tecla grafiteshadow-key
Tecla amarelashadow-key-yellow
--shadow-key: 0 1px 0 0 rgb(255 255 255 / 0.06) inset, 0 -2px 0 0 rgb(0 0 0 / 0.35) inset, 0 6px 0 0 #141213, 0 14px 28px -10px rgb(0 0 0 / 0.7);
--shadow-key-pressed: 0 1px 0 0 rgb(255 255 255 / 0.04) inset, 0 -1px 0 0 rgb(0 0 0 / 0.3) inset, 0 1px 0 0 #141213, 0 4px 10px -6px rgb(0 0 0 / 0.7);
--shadow-key-yellow: 0 1px 0 0 rgb(255 255 255 / 0.45) inset, 0 -2px 0 0 rgb(0 0 0 / 0.18) inset, 0 6px 0 0 #a08c00, 0 18px 40px -12px rgb(242 214 17 / 0.45);
--shadow-key-yellow-pressed: 0 1px 0 0 rgb(255 255 255 / 0.35) inset, 0 -1px 0 0 rgb(0 0 0 / 0.15) inset, 0 1px 0 0 #a08c00, 0 6px 18px -8px rgb(242 214 17 / 0.5);
--shadow-glow: 0 0 0 1px rgb(242 214 17 / 0.4), 0 0 32px 4px rgb(242 214 17 / 0.25);O movimento conta a história das teclas: afundar, acender, digitar. Use GSAP de @/lib/gsap dentro de gsap.matchMedia() com FULL_MOTION_QUERY, e anime só transform, opacity e clip-path.
Pressionar
ease-pressTeclas afundando e voltando. Rápida no início, sem rebote.
Entrada
ease-out-expoEntradas, revelações e luz que se espalha. Chega rápido e assenta devagar.
Transição
ease-in-out-quartMovimentos de um ponto a outro, como painéis que trocam de lugar.
150ms
press
Uma tecla afundando.
300ms
interface
Hover, troca de cor, ícones.
500ms
interface-slow
Abrir e fechar, linhas de foco.
800ms
entrance
Entrada de blocos de texto.
1200ms
entrance-slow
Entrada de títulos e imagens grandes.
"use client";
import { useRef } from "react";
import { FULL_MOTION_QUERY, gsap, useGSAP } from "@/lib/gsap";
export function Section() {
const sectionRef = useRef<HTMLElement>(null);
useGSAP(
() => {
const media = gsap.matchMedia();
media.add(FULL_MOTION_QUERY, () => {
gsap.from("[data-key]", {
y: -22,
opacity: 0,
stagger: 0.07,
ease: "expo.out",
scrollTrigger: { trigger: sectionRef.current, start: "top 80%", once: true },
});
});
},
{ scope: sectionRef },
);
return <section ref={sectionRef}>…</section>;
}Composições assimétricas em 12 colunas, alinhadas à esquerda. Nada é centralizado por padrão: centralize só num momento único. O conteúdo vive no Container, com até 1440px e margem de 20px no celular e 32px a partir de 640px.
Título e introdução
Cabeçalho de seção: título em 7 colunas, texto alinhado à base nas últimas 5.
Tabuleiro de serviços
Teclas de larguras diferentes, como uma fileira de teclado: 7 e 5, depois 4, 3 e 5.
Conteúdo e apoio
Formulário ou demonstração em 8 colunas, contexto em 4.
| Prefixo | Largura | O que muda |
|---|---|---|
sm: | 640px | Margem lateral passa de 20px para 32px. |
md: | 768px | Grids de duas colunas. |
lg: | 1024px | Grid de 12 colunas e navegação completa. |
xl: | 1280px | Composições largas e seções fixadas. |
2xl: | 1536px | Telas grandes. O container para em 1440px. |
import { Container } from "@/components/ui/container";
<section className="bg-charcoal-900 py-24 sm:py-32 lg:py-40">
<Container className="grid gap-6 lg:grid-cols-12 lg:items-end lg:gap-8">
<h2 className="lg:col-span-7">…</h2>
<p className="lg:col-span-5">…</p>
</Container>
</section>Um traço só, desenhado na grade de 20px com espessura 1,8 e pontas redondas, como o T e o W do logo. Os ícones vêm de src/components/ui/icons.tsx e herdam a cor do texto.
ArrowDownIconArrowRightIconArrowUpRightIconCheckIconCloseIconDownloadIconPlusIconSparkIconimport { ArrowRightIcon, CloseIcon } from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
<Button href="/#contato" icon={<ArrowRightIcon />}>
Começar um projeto
</Button>
// Ícones já são aria-hidden. Sem texto visível, o controle precisa de um nome.
<button type="button" aria-label="Fechar menu">
<CloseIcon />
</button>import { Keycap, TILE_RADIUS_RATIO } from "@/components/ui/keycap";A tecla em sua forma mais simples: um quadrado com o canto do logo, uma borda inferior e um rótulo curto. Serve para números de etapa, marcadores e o ícone que acompanha um item. Não é clicável por si só.
Grafite
Grafite pressionado
Amarelo
Amarelo pressionado
Com ícone
Face
borderRadius: side × TILE_RADIUS_RATIOBorda inferior
shadow-key / shadow-key-yellowRótulo
text-sm a text-4xlimport { Keycap } from "@/components/ui/keycap";
<Keycap size="sm" tone="outline" className="text-sm tabular-nums">
01
</Keycap><Keycap size="sm" tone={open ? "yellow" : "charcoal"} pressed={open}>
<PlusIcon className={open ? "rotate-[135deg]" : undefined} />
</Keycap>import { TILE_RADIUS_RATIO } from "@/components/ui/keycap";
const style = { width: side, height: side, borderRadius: side * TILE_RADIUS_RATIO };| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "md" | Lado de 44, 64, 96 ou 144px. O raio é sempre 14,3% do lado. |
tone | "charcoal" | "yellow" | "outline" | "charcoal" | Grafite com borda, amarelo aceso com luz por baixo, ou vazado com contorno. |
pressed | boolean | false | Afunda 5px e troca a sombra pela versão pressionada. O vazado afunda 2px. |
className | string | nenhum | Classes extras, como tamanho de texto para rótulos longos. |
children | ReactNode | nenhum | Um caractere, um número curto ou um ícone. |
text-muted, que passa em AA nos dois temas.import { LitKey } from "@/components/ui/lit-key";Uma tecla decorativa feita em camadas para ser animada: brilho, borda, face e luz. Pressionar e acender usam só transform e opacidade, por isso é a peça das animações de teclas que se apertam sozinhas.
Apagada
Acesa, fill
Acesa, glow
Pressionada
Retangular
Brilho
[data-lit-key-glow]Borda
edgeFace
[data-lit-key-cap]Luz
[data-lit-key-light]import { LitKey } from "@/components/ui/lit-key";
<LitKey side={56} edge={5} lit light="fill" capClassName="font-display text-xl font-semibold">
9
</LitKey>const cap = key.querySelector("[data-lit-key-cap]");
const light = key.querySelectorAll("[data-lit-key-light], [data-lit-key-glow]");
gsap
.timeline()
.to(cap, { y: 4, duration: 0.07, ease: "power2.in" })
.to(light, { autoAlpha: 1, duration: 0.08 }, "<")
.to(cap, { y: 0, duration: 0.42, ease: "back.out(2.6)" });| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
side | number | nenhum | Lado em px de uma tecla quadrada. O raio segue a proporção do bloco. Sem side, a tecla se ajusta ao conteúdo. |
radius | number | 8 | Raio em px para teclas que não são quadradas. |
edge | number | 3 | Altura da borda inferior em px. Pressionada, a face desce edge − 1. |
lit | boolean | false | Acende a camada de luz e o brilho por baixo. |
light | "fill" | "glow" | "fill" | fill pinta a face de amarelo. glow mantém o grafite e deixa a luz vazar de baixo. |
pressed | boolean | false | Afunda a face com transform em linha, que o GSAP consegue ler e devolver. |
capClassName | string | nenhum | Classes da face: tamanho e família da fonte, largura de teclas retangulares. |
className | string | nenhum | Classes do contêiner, como margens e alinhamento. |
children | ReactNode | nenhum | O rótulo. É renderizado duas vezes, na face e na camada de luz, para trocar de cor só com opacidade. |
Repassa as outras props de <span> ao contêiner, como data-* para animação e style.
gsap.matchMedia() com FULL_MOTION_QUERY. Com movimento reduzido, mostre o estado final com lit e pressed.import { KeySequence } from "@/components/ui/key-sequence";Teclas gigantes que caem, se apertam sozinhas uma depois da outra e deixam uma acesa. Depois a legenda se digita. É o movimento de páginas de um momento só, como a 404.
--key-side.Teclas que se apertam sozinhas.
Passe o cursor pelas teclas ou aperte 9 no teclado.
Fileira
labelTecla
[data-key] [data-key-cap] [data-key-glow]Tecla acesa
litIndexLegenda
data-typed, data-typed-caret, data-revealimport { KeySequence } from "@/components/ui/key-sequence";
<KeySequence keys={["4", "0", "4"]} litIndex={1} label="Erro 404">
<h1 data-typed>
Esta página não existe.
<span data-typed-caret aria-hidden className="animate-caret …" />
</h1>
<p data-reveal>O endereço mudou ou nunca existiu.</p>
</KeySequence>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
keysobrigatória | readonly string[] | nenhum | Um caractere por tecla, na ordem em que são digitadas. |
labelobrigatória | string | nenhum | Nome acessível da fileira, que é exposta como uma única imagem. |
litIndex | number | nenhum | Índice da tecla que termina acesa em amarelo, como o bloco do logo. |
children | ReactNode | nenhum | Legenda depois das teclas. data-typed aparece letra a letra, data-typed-caret pisca no fim e data-reveal entra por último. |
className | string | nenhum | Classes do contêiner externo. |
import { Container } from "@/components/ui/container";A caixa que alinha todo conteúdo: largura máxima de 1440px, centralizada, com 20px de margem no celular e 32px a partir de 640px. O fundo da seção fica fora dele e vai de ponta a ponta.
Conteúdo da seção
Até 1440px de largura, sempre alinhado à esquerda do grid.
Largura máxima
mx-auto max-w-[1440px]Margem lateral
px-5 sm:px-8import { Container } from "@/components/ui/container";
<section aria-labelledby="servicos-titulo" className="bg-charcoal-900 py-24 sm:py-32 lg:py-40">
<Container className="grid gap-6 lg:grid-cols-12 lg:gap-8">…</Container>
</section>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
className | string | nenhum | Mesclado com o padrão. Use para grid e espaçamento interno, não para mudar a largura máxima. |
children | ReactNode | nenhum | O conteúdo da seção. |
Aceita todas as props de <div>.
import { ResponsiveImage, type MediaName } from "@/components/ui/responsive-image";Imagens otimizadas para o site exportado estático: um <picture> com AVIF e WebP em duas larguras. Sem next/image, porque o export não otimiza na hora.

keyfield

symbol3d

service-agents

service-automation

service-web

service-apps

service-platforms

service-design

mockup-stationery

mockup-signage

mockup-hoodie

mockup-phone
Fontes
/media/<name>-960.avifImagem
object-coverCarregamento
priorityimport { ResponsiveImage } from "@/components/ui/responsive-image";
<ResponsiveImage
name="service-agents"
alt=""
sizes="(min-width: 1024px) 34vw, 100vw"
className="aspect-square"
/>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
nameobrigatória | MediaName | nenhum | Uma das 12 mídias de public/media. Cada uma existe em 960 e 1920px, AVIF e WebP. |
altobrigatória | string | nenhum | Descreve a imagem. Use "" quando ela só repete o texto ao lado. |
sizes | string | "100vw" | Quanto da tela a imagem ocupa, para o navegador escolher entre 960 e 1920. |
className | string | nenhum | Classes do <picture>. |
imageClassName | string | nenhum | Classes do <img>, como object-position. |
priority | boolean | false | Carrega já, com fetchpriority alta. Só para a imagem principal acima da dobra. |
import { VideoLoop } from "@/components/ui/video-loop";Vídeo decorativo em loop, sem som. Só toca enquanto está na tela, pausa ao sair e fica no pôster com movimento reduzido ou com as animações pausadas no site. Carrega o WebM primeiro e cai para MP4, com um corte de 1280px para telas menores.
Pôster
/media/<name>-loop-poster.webpFontes
<source type media>, preload="none"Observador
IntersectionObserverimport { VideoLoop } from "@/components/ui/video-loop";
<div className="relative aspect-video overflow-hidden rounded-xl">
<VideoLoop name="keyfield" />
</div>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
nameobrigatória | "keyfield" | "symbol" | nenhum | Qual loop tocar. Cada um tem WebM VP9, MP4 e um pôster. |
className | string | nenhum | Classes do pôster e do <video>, que já cobrem a caixa com object-cover. |
priority | boolean | false | Carrega o pôster eager e com prioridade alta, em vez de lazy. Use só quando o vídeo aparece acima da dobra. |
import { Marquee } from "@/components/ui/marquee";Uma fileira em loop contínuo, movida pelo ticker do GSAP. Acelera com a rolagem, troca de sentido quando a pessoa sobe e só roda enquanto está na tela. É decorativa: o mesmo conteúdo precisa existir em texto para leitores de tela.
Contêiner
overflow-hidden aria-hiddenTrilho
[data-marquee-track]Cópias
copies, copyClassNameimport { Marquee } from "@/components/ui/marquee";
import { setMotionPaused, useMotionPaused } from "@/lib/motion-preference";
const paused = useMotionPaused();
<ul className="sr-only">
{technologies.map((name) => <li key={name}>{name}</li>)}
</ul>
<Marquee paused={paused} copyClassName="py-2">
{technologies.map((name) => <span key={name}>{name}</span>)}
</Marquee>
<button type="button" aria-pressed={paused} onClick={() => setMotionPaused(!paused)}>
Pausar movimento
</button>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
childrenobrigatória | ReactNode | nenhum | Uma cópia do conteúdo. O componente repete e emenda as cópias. |
direction | "left" | "right" | "left" | Sentido do loop. Inverte quando a pessoa rola para cima. |
pixelsPerSecond | number | 32 | Velocidade base. A velocidade da rolagem soma até 6 vezes isso. |
copies | number | 3 | Quantas cópias ficam lado a lado, no mínimo 2. Aumente se uma cópia for mais estreita que a tela. |
paused | boolean | false | Desacelera até parar, sem tranco. |
className | string | nenhum | Classes do contêiner, que corta o que passa da borda. |
copyClassName | string | nenhum | Classes de cada cópia, como padding vertical para a borda das teclas. |
import { MotionToggle } from "@/components/ui/motion-toggle";O interruptor que para todo movimento automático do site: vídeos em loop, fileiras de tecnologias, teclas que se apertam sozinhas e cursores piscando. Fica no cabeçalho e no menu do celular. A escolha vale para todas as páginas e continua na próxima visita.
Tecla
shadow-key → shadow-key-pressedÍcone
text-yellow-500Rótulo
showLabelPreferência
data-motion="paused"import { MotionToggle } from "@/components/ui/motion-toggle";
<MotionToggle className="hidden lg:inline-flex" />
<MotionToggle showLabel className="mb-4 self-start" />import { FULL_MOTION_QUERY, gsap } from "@/lib/gsap";
import { pauseWithMotionPreference } from "@/lib/motion-preference";
media.add(FULL_MOTION_QUERY, () => {
const loop = gsap.timeline({ repeat: -1 });
…
return pauseWithMotionPreference(loop);
});| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
showLabel | boolean | false | Mostra o rótulo ao lado do ícone. Sem ele, o botão é um quadrado de 40px com o nome em sr-only e num title. |
className | string | nenhum | Classes do botão, como a visibilidade por breakpoint e as margens. |
pauseWithMotionPreference, subscribeMotionPaused ou useMotionPaused de @/lib/motion-preference.import { Accordion, type AccordionPanel } from "@/components/ui/accordion";Perguntas e respostas que abrem no lugar. O + de cada item é uma tecla: ao abrir, ela afunda, acende em amarelo e gira até virar ×. Segue o padrão de accordion da WAI-ARIA.
Depende do escopo. Um site institucional e um sistema com integrações têm tamanhos bem diferentes.
Por isso começamos com uma conversa sobre o que você precisa. Depois dela, você recebe uma proposta com preço fechado, sem surpresa no meio do caminho.
Sites ficam prontos em algumas semanas. Sistemas, aplicativos e agentes de IA levam alguns meses, conforme o tamanho.
O cronograma vem na proposta, dividido em entregas. Você acompanha cada etapa e testa o que já está pronto.
Pode, como qualquer pessoa. Por isso o agente trabalha com limites claros: ele sabe o que pode fazer sozinho e quando deve chamar alguém da sua equipe.
Antes de ir ao ar, testamos com conversas reais do seu negócio. Depois, um painel mostra cada atendimento para você revisar e corrigir o rumo.
Título
headingLevelBotão
[data-accordion-trigger]Tecla
[data-accordion-key]Resposta
role="region" inertLinha de luz
scale-x-100import { Accordion } from "@/components/ui/accordion";
<Accordion
headingLevel={3}
panels={FAQ.map((item) => ({ id: item.id, heading: item.question, body: <p>{item.answer}</p> }))}
onToggle={(id, open) => open && pressQuestionKey()}
/>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
panelsobrigatória | readonly AccordionPanel[] | nenhum | Itens com id, heading e body. O id vira parte dos ids dos controles. |
headingLevel | 2 | 3 | 4 | 3 | Nível do título que envolve cada botão. Siga a hierarquia da página. |
defaultOpenIds | readonly string[] | [] | Itens que começam abertos. Vários podem ficar abertos ao mesmo tempo. |
onToggle | (id: string, open: boolean) => void | nenhum | Avisa quando um item abre ou fecha, para animar algo em volta. |
className | string | nenhum | Classes da lista. |
headingLevel pela hierarquia: dentro de uma seção com h2, os itens são h3.import { TextField } from "@/components/ui/text-field";Campo de uma linha com rótulo, dica e erro já ligados. O campo é um poço de tecla: plano, com uma borda inferior sólida que acende em amarelo quando recebe foco.
Vazio
Preenchido
Se for um projeto pessoal, deixe em branco.
Opcional com dica
Digite o número com DDD.
Erro
Desativado
Digite um e-mail válido.
Rótulo
label, optionalPoço
fieldControlClasses(surface)Linha de foco
peer-focus:scale-x-100Dica e erro
hint, errorimport { TextField } from "@/components/ui/text-field";
<TextField
name="email"
type="email"
label="E-mail"
autoComplete="email"
inputMode="email"
required
error={fieldErrors.email}
/>import { FieldShell, fieldControlClasses, useFieldA11y } from "@/components/ui/text-field";
const field = useFieldA11y({ id, hint, error });
<FieldShell label="Orçamento" hint={hint} error={error} field={field}>
<select id={field.controlId} aria-describedby={field.describedBy} aria-invalid={field.invalid} className={fieldControlClasses("charcoal", "h-14")}>…</select>
</FieldShell>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
labelobrigatória | string | nenhum | Rótulo visível, ligado ao campo por htmlFor. |
optional | boolean | false | Mostra "opcional" ao lado do rótulo. Campos sem a marca são obrigatórios. |
hint | string | nenhum | Dica abaixo do campo, ligada por aria-describedby. |
error | string | nenhum | Mensagem de erro. Liga aria-invalid, pinta a borda e entra no aria-describedby. |
surface | "charcoal" | "accent" | "charcoal" | charcoal para o site escuro, accent para o painel amarelo do contato. Cada uma tem cores de texto e erro com contraste próprio. |
className | string | nenhum | Classes do bloco inteiro: rótulo, campo e mensagens. Use para colunas do grid. |
inputClassName | string | nenhum | Classes do <input>. |
id | string | useId() | Gerado quando omitido. Os ids da dica e do erro derivam dele. |
type | string | "text" | Use email, tel e url com o inputMode e o autoComplete certos. |
Repassa as outras props de <input>: name, required, autoComplete, inputMode, maxLength, placeholder, defaultValue, disabled.
#7a1608, que fica em 7,4:1 sobre o amarelo.import { TextArea } from "@/components/ui/text-area";O mesmo poço do TextField para textos longos. Cresce com o conteúdo onde o navegador suporta field-sizing e vira uma caixa redimensionável nos outros.
Conte um pouco mais. Pelo menos 10 caracteres.
Rótulo, dica e erro
FieldShellCaixa
min-h-36 max-h-[26rem] [field-sizing:content]import { TextArea } from "@/components/ui/text-area";
<TextArea
name="message"
label="Mensagem"
required
maxLength={2000}
placeholder="O que você quer construir, para quem e o que já existe hoje."
error={fieldErrors.message}
/>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
labelobrigatória | string | nenhum | Rótulo visível, ligado ao campo por htmlFor. |
optional | boolean | false | Mostra "opcional" ao lado do rótulo. Campos sem a marca são obrigatórios. |
hint | string | nenhum | Dica abaixo do campo, ligada por aria-describedby. |
error | string | nenhum | Mensagem de erro. Liga aria-invalid, pinta a borda e entra no aria-describedby. |
surface | "charcoal" | "accent" | "charcoal" | charcoal para o site escuro, accent para o painel amarelo do contato. Cada uma tem cores de texto e erro com contraste próprio. |
className | string | nenhum | Classes do bloco inteiro. |
textareaClassName | string | nenhum | Classes do <textarea>, como altura mínima. |
id | string | useId() | Gerado quando omitido. |
rows | number | 5 | Altura inicial. Onde há field-sizing, a caixa cresce com o texto até 416px. |
Repassa as outras props de <textarea>, como name, required, maxLength e placeholder.
import { ChoiceChip, ChoiceChipGroup } from "@/components/ui/choice-chip";Escolhas múltiplas em forma de tecla. Cada chip é um checkbox de verdade: em repouso fica erguido com um +, marcado afunda, acende e troca o + por um visto.
Grupo
ChoiceChipGroupTecla
has-[:checked]:translate-y-[3px]Checkbox
type="checkbox" sr-onlyÍcone
PlusIcon / CheckIconimport { ChoiceChip, ChoiceChipGroup } from "@/components/ui/choice-chip";
<ChoiceChipGroup legend="Do que você precisa?" optional hint="Marque quantas opções quiser." hintId={`${id}-hint`} errorId={`${id}-error`}>
{CONTACT_NEEDS.map((need) => (
<ChoiceChip key={need} name="needs" value={need} label={need} />
))}
</ChoiceChipGroup>ChoiceChip
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
labelobrigatória | string | nenhum | Texto do chip, que também é o rótulo do checkbox. |
surface | "charcoal" | "accent" | "charcoal" | charcoal para o site escuro, accent para o painel amarelo do contato. Cada uma tem cores de texto e erro com contraste próprio. |
className | string | nenhum | Classes do <label> que desenha a tecla. |
checked | boolean | nenhum | Controlado. Para não controlado, use defaultChecked. |
disabled | boolean | false | Opacidade 45% e cursor de bloqueio. |
Repassa as props de <input type=checkbox>: name, value, defaultChecked, onChange.
ChoiceChipGroup
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
legendobrigatória | string | nenhum | A pergunta, num <legend> do <fieldset>. |
optional | boolean | false | Mostra "opcional" ao lado da legenda. |
hint | string | nenhum | Dica abaixo dos chips. |
error | string | nenhum | Erro do grupo. |
errorIdobrigatória | string | nenhum | Id da mensagem de erro, ligado ao fieldset por aria-describedby. |
hintIdobrigatória | string | nenhum | Id da dica. |
surface | "charcoal" | "accent" | "charcoal" | charcoal para o site escuro, accent para o painel amarelo do contato. Cada uma tem cores de texto e erro com contraste próprio. |
className | string | nenhum | Classes do <fieldset>. |
childrenobrigatória | ReactNode | nenhum | Os ChoiceChip. |
import { FieldMessage, ACCENT_ERROR_CLASSES } from "@/components/ui/field-message";A linha de texto abaixo de um campo: dica ou erro. Os campos já usam por dentro; use direto só em controles próprios.
Marque quantas opções quiser.
Digite um e-mail válido.
Marque quantas opções quiser.
Digite um e-mail válido.
Ícone
tone=errorTexto
text-smimport { FieldMessage } from "@/components/ui/field-message";
<FieldMessage id={`${id}-error`} tone="error">
Escolha uma data a partir de amanhã.
</FieldMessage>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
idobrigatória | string | nenhum | Referenciado pelo aria-describedby do campo. |
tone | "hint" | "error" | "hint" | error adiciona ícone e peso semibold. |
surface | "charcoal" | "accent" | "charcoal" | charcoal para o site escuro, accent para o painel amarelo do contato. Cada uma tem cores de texto e erro com contraste próprio. |
className | string | nenhum | Classes do parágrafo. |
childrenobrigatória | ReactNode | nenhum | A mensagem, numa frase. |
import { Logo } from "@/components/brand/logo";O logo desenhado em SVG a partir da geometria oficial, em três arranjos e cinco paletas. As regras de uso, como área de proteção e tamanhos mínimos, estão no manual da marca.
color Sobre carvão. A versão principal.
color-light Sobre fundos claros.
black Impressão em uma cor.
white Sobre fotos escuras e uma cor.
current O wordmark herda a cor do texto.
Wordmark
data-logo-part="wordmark"Bloco
data-logo-part="tile"Nove
data-logo-part="nine"Registro e assinatura
data-logo-part="registered" / "tagline"import { Logo } from "@/components/brand/logo";
<Link href="/" aria-label="Two9, página inicial">
<Logo layout="horizontal" decorative className="w-[118px]" />
</Link>| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
layout | "full" | "horizontal" | "symbol" | "horizontal" | full traz a assinatura embaixo. symbol é só o bloco do 9. |
variant | "color" | "color-light" | "black" | "white" | "current" | "color" | Paleta do desenho. current usa currentColor no wordmark. |
title | string | "Two9" | Nome acessível do SVG, num <title>. |
className | string | nenhum | Defina a largura; a altura segue a proporção. |
decorative | boolean | false | Esconde o logo de leitores de tela, para quando um link em volta já tem nome. |
A abertura de cada página: o h1, uma frase de apoio, uma ação principal e um visual de teclas. As letras do título sobem enquanto as teclas se apertam.
Software que trabalha por você.
Uma frase que diz o que a página entrega, em até três linhas.
Título
font-display clamp até 7.5remApoio
max-w-[34rem]Ações
Button lgVisual
aria-hidden<section aria-labelledby="hero-title" className="relative isolate overflow-hidden bg-charcoal-800">
{/* Visual: teclas, vídeo ou cena, sempre aria-hidden */}
<Container className="pb-16 pt-32 lg:pb-24">
<h1 id="hero-title" className="max-w-[13ch] font-display text-[clamp(2.6rem,7.4vw,7.5rem)] leading-[0.98] tracking-[-0.045em]">…</h1>
<p className="mt-7 max-w-[34rem] text-md sm:text-lg">…</p>
<div className="mt-10 flex flex-wrap items-center gap-x-6 gap-y-4">
<Button href="/#contato" size="lg" icon={<ArrowRightIcon />}>Começar um projeto</Button>
<Button href="/#servicos" variant="ghost" size="lg" icon={<ArrowDownIcon />}>Ver serviços</Button>
</div>
</Container>
</section>Cada serviço é uma tecla larga num tabuleiro afundado. No hover ou no foco, a face desce 7px e a luz amarela vaza pela borda de baixo. A prévia usa o componente real da página inicial.
Atendentes, SDRs e assistentes internos que respondem, qualificam e executam tarefas no WhatsApp, no site e nos seus sistemas, 24 horas por dia.
Fluxos que leem documentos, preenchem planilhas, atualizam o CRM e avisam a equipe. O trabalho repetitivo sai da mão das pessoas.
Base
KeyBaseFace
KEY_FACE_CLASSESImagem
ResponsiveImage alt=""Texto
ServiceTagsAro
KeyRimimport { KEY_FACE_CLASSES, KEY_SLOT_CLASSES, KeyBase, KeyRim } from "@/components/home/services-tile";
<li className={KEY_SLOT_CLASSES}>
<KeyBase />
<article aria-labelledby={titleId} className={cn(KEY_FACE_CLASSES, "bg-charcoal-800")}>
<ResponsiveImage name="service-agents" alt="" … />
<h3 id={titleId}>Agentes de IA</h3>
<p>…</p>
</article>
<KeyRim />
</li>O formulário de contato, num painel amarelo que é o momento deliberado do site. Campos em duas colunas, uma pergunta de múltipla escolha e um único botão grafite que contrasta com o amarelo.
Digite um e-mail válido.
Painel
bg-yellow-500 shadow-key-yellowCampos
surface="accent"Escolhas
ChoiceChipGroupEnvio
variant="secondary"<form noValidate onSubmit={handleSubmit} className="grid gap-x-6 gap-y-8 sm:grid-cols-2">
<TextField name="name" label="Nome" autoComplete="name" required error={errors.name} surface="accent" />
<TextField name="email" type="email" label="E-mail" autoComplete="email" required error={errors.email} surface="accent" />
<ChoiceChipGroup legend="Do que você precisa?" optional hintId={hintId} errorId={errorId} surface="accent" className="sm:col-span-2">
{CONTACT_NEEDS.map((need) => <ChoiceChip key={need} name="needs" value={need} label={need} surface="accent" />)}
</ChoiceChipGroup>
<TextArea name="message" label="Mensagem" required error={errors.message} surface="accent" className="sm:col-span-2" />
<Button type="submit" variant="secondary" size="lg">Enviar mensagem</Button>
</form>Título grande à esquerda, introdução curta à direita, as duas alinhadas pela base. As linhas do título sobem por uma máscara quando a seção entra.
O que a Two9 constrói
Do agente que atende no WhatsApp ao sistema que organiza a operação, a Two9 projeta e desenvolve cada peça com o mesmo time.
Título
lg:col-span-7Introdução
lg:col-span-5 lg:items-end<Container className="grid gap-6 lg:grid-cols-12 lg:items-end lg:gap-8">
<h2 id="servicos-titulo" className="font-display text-[clamp(2.25rem,5vw,4.5rem)] leading-[1.02] tracking-[-0.04em] lg:col-span-7">
O que a Two9 constrói
</h2>
<p className="max-w-[34rem] text-base sm:text-md lg:col-span-5 lg:col-start-8 lg:pb-2">…</p>
</Container>Os 137 tokens também existem em tokens/two9.tokens.json, no formato do W3C Design Tokens Community Group. Serve para levar cores, medidas e sombras para o Figma, para apps nativos ou para qualquer ferramenta que leia o padrão.
{
"color": {
"yellow": {
"500": {
"$type": "color",
"$value": {
"colorSpace": "srgb",
"components": [
0.949,
0.8392,
0.0667
],
"hex": "#f2d611"
},
"$description": "Amarelo Two9. A luz. Ação, destaque e o bloco do logo.",
"$extensions": {
"br.com.two9": {
"cssVariable": "--color-yellow-500"
}
}
}
}
},
"theme": {
"light": {
"accent-text": {
"$type": "color",
"$value": "{color.yellow.800}",
"$description": "Destaque como texto. Amarelo usado como texto. No tema claro vira 800.",
"$extensions": {
"br.com.two9": {
"cssVariable": "--accent-text",
"utility": "text-accent-text"
}
}
}
}
},
"easing": {
"press": {
"$type": "cubicBezier",
"$value": [
0.3,
0.7,
0.4,
1
],
"$description": "Pressionar. Teclas afundando e voltando. Rápida no início, sem rebote.",
"$extensions": {
"br.com.two9": {
"utility": "ease-press",
"gsap": "power2.out"
}
}
}
}
}src/app/globals.css. O código e esta página leem src/design-system/tokens.ts.# Confere globals.css, tokens.ts e o JSON
pnpm exec vitest run src/design-system/tokens.test.ts
# Depois de mudar um token, regenera tokens/two9.tokens.json
pnpm exec vitest run src/design-system/tokens.test.ts -u