Brand tokens
Four hex values and one gradient derive all the CSS through color-mix; swapping the palette means editing one block.
Get started
Foundations
Components
Conventions
Changelog
The EchoSistema starter guide: how to run it, where everything lives and how to use the system's tokens and components.
The Nuxt starter for the group's platform sites: the whole Liquid Glass parameterized by four brand tokens.
Four hex values and one gradient derive all the CSS through color-mix; swapping the palette means editing one block.
Automatic parity: every themeable value has a dark twin, and the right theme paints before first paint.
Canonical Spanish at the root, pt-BR and English prefixed, a single catalog and locales in lockstep.
The browser's dialog, details and selects dressed by the system: accessibility for free.
pnpm installs, runs and validates. The same scripts as package.json, no hidden steps.
| Command | Role |
|---|---|
pnpm dev | Development server with hot reload. |
pnpm build | Production build (SSR). |
pnpm generate | Prerendered static site. |
pnpm preview | Serves the production build locally. |
pnpm lint | ESLint across the whole project. |
pnpm typecheck | vue-tsc over the app and the configuration. |
Standard Nuxt 4 layout: app/ is the site, i18n/ the texts, shared/ what server and client share.
app/ assets/css/main.css components/ composables/ data/ layouts/ pages/ plugins/ utils/ i18n/locales/ shared/ nuxt.config.ts
main.css is the single style entry point: .vue files load no style blocks. The language catalog lives in shared/languages.ts and the navigation in app/data/nav.ts.
The MARCA block at the top of main.css is the only place with brand hex values.
Backgrounds, text, glass, buttons and the code panel all derive from these four tokens through color-mix, with automatic light/dark parity. To rebrand a derived site, swap this block and nothing else; the agro and consumer palettes sit commented in the file as examples.
:root {
--brand-1: #426ab2;
--brand-2: #40c1c3;
--brand-deep: #040b2d;
--brand-link: #3d91cf;
}The theme flips on html[data-theme]; the language owns the URL.
data-theme
A pre-paint script in the head reads the stored preference (or the system's) and sets data-theme before first paint: no flash of the wrong theme. Runtime switching is the same attribute.
prefix_except_default
Canonical Spanish lives unprefixed; /pt-BR and /en prefix the other routes. Adding a language is one entry in the shared/languages.ts catalog plus the matching locale file.
Semantic content is dressed by its container: .es-prose styles the elements inside it.
Headings, paragraphs, quotations, code and the other text elements pick up the system's style by entering an .es-prose, with no per-element utilities. The loaded families are Manrope, Montserrat and JetBrains Mono.
<div class="es-prose">
<h2>…</h2>
<p>…</p>
<blockquote>…</blockquote>
</div>The 41 components in app/components/echo, by role. Each dresses a native element when one exists: that is where keyboard, focus and announcement come from, with no reimplementation.
Button · Chip · Badge · Pill · Dropdown · Pagination · Breadcrumb · Tabs
Field · Input · Textarea · Select · SearchSelect · TagsInput · Checkbox · Radio · Switch · Slider · Rating · FileButton · Dropzone · DatePicker · PhoneInput
Alert · Modal · Drawer · Toaster · Block · Skeleton · Spinner · Progress · Ring
Card · List · ListItem · DataTable · Avatar · Gallery · Tree · Timeline · Collapse · Divider · Stat · Player
Chart · Map · Carousel · Editor · ImageCropper · ImageViewer · QrCode · Barcode
Two axes never combined by hand: .es-i--* provides the color, the variant consumes it.
Ten intents define local tokens (--i, --i-on, --i-grad and derivatives) consumed by the four label families and the modal. The variants (filled, gradient, outline, glass, ghost) are each written once.
<echo-button variant="filled" intent="danger">…</echo-button>
<echo-chip variant="soft" intent="info">…</echo-chip>
<echo-badge variant="filled" intent="success">…</echo-badge>
<echo-pill variant="outline" intent="caution">…</echo-pill>Eight tones over the same palette, with the accent wedge on the edge.
Four semantic status tones (info, success, warn, danger) and four named tonal ones (caution, steel-blue, navy-blue, dark). Text stays in the theme's grays; the accent goes into the icon, the title and the wedge.
<echo-alert tone="success" title="…">
…
</echo-alert>The native dialog: trapped focus, Esc, inert and blurred background.
An opaque theme surface, an intent wedge like the alerts, a background blurred into illegibility while open and locked scrolling. Clicking outside does not close; Esc, the header close and the footer actions do. Width follows the sm, md, lg, xl and xxl scale.
<echo-modal ref="modal" title-id="…"
intent="warning" size="lg">
<template #title>…</template>
…
<template #footer>…</template>
</echo-modal>
modal.value?.open()The shortest line that puts each component on screen, ready to copy.
Every component carries the same snippet in its own file, in a "Copy and paste" block right above the code — that is where the full prop list lives. What is here are the minimum usages, to find what fits without opening seven files.
<echo-button variant="filled" intent="primary">Save</echo-button>
<echo-chip>tag</echo-chip> · <echo-badge intent="success">ok</echo-badge> · <echo-pill>4</echo-pill>
<echo-dropdown label="Account"><template #trigger>…</template>…</echo-dropdown>
<echo-tabs v-model="tab" :tabs="TABS" />
<echo-pagination v-model="page" :pages="12" />
<echo-breadcrumb :items="TRAIL" /><echo-field v-slot="{ id, fieldName }" label="E-mail" name="email"><echo-input :id="id" v-model="email" :name="fieldName" /></echo-field>
<echo-input v-model="text" type="email" size="sm" />
<echo-textarea v-model="note" rows="3" />
<echo-select v-model="topic"><option value="a">A</option></echo-select>
<echo-search-select v-model="country" list-id="countries" :options="names" />
<echo-date-picker v-model="birthday" mode="date" :start-year="1990" />
<echo-phone-input v-model="phone" country="br" @validity="ok = $event" />
<echo-checkbox v-model="agreed">I agree</echo-checkbox>
<echo-radio v-model="skin" name="skin" value="bordered" />
<echo-switch v-model="on" />
<echo-slider v-model="zoom" :min="1" :max="4" :step="0.01" />
<echo-rating v-model="stars" />
<echo-tags-input v-model="tags" />
<echo-file-button accept="image/*" @files="onFiles">Choose</echo-file-button>
<echo-dropzone accept="image/*" @files="onFiles">…</echo-dropzone><echo-alert tone="danger" title="That failed">…</echo-alert>
<echo-modal ref="el" title-id="x-title" size="md"><template #title>…</template>…</echo-modal>
<echo-drawer ref="el" title-id="y-title">…</echo-drawer>
useToast().push({ tone: 'success', title: 'Done', message: 'Saved.' })
<echo-block :loading="pending" label="Loading…">…</echo-block>
<echo-skeleton :lines="3" /> · <echo-spinner size="sm" />
<echo-progress :value="64" label="Progress" /> · <echo-ring :value="76" /><echo-card title="Team" subtitle="Who works here" :loading="pending">…</echo-card>
<echo-list><echo-list-item title="Row" /></echo-list>
<echo-data-table :rows="ROWS" :columns="COLS" />
<echo-avatar :src="url" name="Ada Lovelace" size="lg" />
<echo-gallery :items="IMAGES" />
<echo-tree :nodes="NODES" /> · <echo-timeline :items="STEPS" />
<echo-collapse><template #summary>…</template>…</echo-collapse>
<echo-stat label="Visits" value="24.8k" :delta="12.4" />
<echo-player :src="video" /><echo-chart :option="option" summary="What the chart says" height="280px" />
<echo-map :lat="-25.5" :lng="-54.6" :zoom="12" :markers="PINS" />
<echo-carousel label="Slides" loop autoplay>…</echo-carousel>
<echo-editor v-model="html" label="Body" />
<echo-image-cropper ref="el" :usages="['avatar']" mask="square" @apply="upload" />
<echo-image-viewer ref="el" :src="url" :caption="url" />
<echo-qr-code value="https://example.com" :size="180" />
<echo-barcode value="ECHO-2026-0001" format="CODE128" /><auth-login-modal ref="el" :pending="sending" :error="failure" @submit="signIn" />
<auth-register-modal ref="el" @submit="register" @login="switchToLogin" />
<auth-forgot-password-modal ref="el" :sent="sent" @submit="recover" />
<auth-update-password-modal ref="el" require-current @submit="changePassword" />
<auth-lock-modal ref="el" :name="user.name" :avatar="user.avatar" @submit="unlock" />Sign in, create account, recover and change the password, and the locked session. Five ready components in app/components/auth/, over the same echo-modal.
All five follow the same contract: open() and close() exposed by ref, one submit with the data, and no network calls inside. The application knows the URL, the token and what to do with the response — the component only collects, validates what doesn't depend on the server, and hands it back.
<auth-login-modal
ref="login"
:pending="sending"
:error="failure"
logo="/img/minha-marca.svg"
title="Entrar no painel"
@submit="signIn"
@forgot="forgot?.open()"
@register="register?.open()"
/>
login.value?.open()The submit comes out in the API's vocabulary, not the internal variables': sending the payload straight to the endpoint is the common case, and renaming fields in every derived site would be the same mapping written many times.
| Component | Submit fields | Events |
|---|---|---|
<auth-login-modal> | email, password, remember | @submit @forgot @register |
<auth-register-modal> | name, email, gender (m|f|o), birth_date (yyyy-mm-dd), password, password_confirmation, terms (1|0) | @submit @login |
<auth-forgot-password-modal> | email | @submit @login |
<auth-update-password-modal> | password, password_confirmation | @submit |
<auth-lock-modal> | password | @submit @sign-out |
The props are the same across all five, with two noted exceptions. All are optional.
logo
Header artwork, when the modal stands for a brand other than the site's. Without it, the site logo.
title
Your own title instead of the flow's default text.
pending
Locks the fields and swaps the button label while your call runs.
error
Server message, shown in an alert above the form. Whoever passed it clears it.
sent
Recovery modal only: swaps the form for the sent confirmation. It belongs to the caller, because whoever made the request is who knows it arrived.
name / avatar
Lock modal only: name and portrait of whoever's session is locked.
Fields over the lens ink and the native details as a card.
es-input covers input and textarea; es-select is the native select with the same ink. es-collapse dresses the browser's details: it opens, closes and stays accessible without a line of JS.
<echo-input v-model="email" type="email" />
<echo-textarea v-model="message" rows="3" />
<echo-select v-model="topic">…</echo-select>
<echo-collapse>
<template #summary>…</template>
…
</echo-collapse>The conventions that hold for every derived site.
No style blocks in .vue: utilities in the template and everything else in main.css.
Raw hex in a template is a finding; the whole brand lives in the MARCA block.
Every system component class and token uses the prefix, renameable in one global replace.
All text checked against the real field; accent color is not text color.
prefers-reduced-motion and prefers-reduced-transparency respected by every animation and every glass.
Canonical Spanish; every interface string through t(), contracts stay literal.
A question the documentation does not cover?
Talk to the EchoSistema group at echosistema.online.
What changed in the template, most recent first.